- docs/PLAN.md — план по этапам, обновляется по мере работы - docs/TECHNICAL.md — слои, карта папок, контракты, рабочие процессы - docs/DECISIONS.md — журнал решений, принятых самостоятельно - разрешение 1080x1920, fit-width (портрет, 4:3..21:9) - каркас assets/scripts (core / modules / app / ui) - headless-тесты на vitest + strict typecheck + защита чистоты core - Rng: детерминированный генератор и перемешивание Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
66 lines
2.1 KiB
TypeScript
66 lines
2.1 KiB
TypeScript
/**
|
|
* Deterministic pseudo-random generator.
|
|
*
|
|
* The whole rules core is reproducible from a seed: given the same seed and the
|
|
* same sequence of actions, a match replays identically. That is what lets a bug
|
|
* report be a single integer, and what keeps the soak tests meaningful.
|
|
*
|
|
* Algorithm is mulberry32 — small, fast, and good enough for shuffling cards.
|
|
* Its entire state is one 32-bit integer, so it serialises into the save/state
|
|
* without ceremony.
|
|
*/
|
|
export class Rng {
|
|
private _state: number;
|
|
|
|
constructor(seed: number) {
|
|
// Force to uint32 so a negative or fractional seed still behaves.
|
|
this._state = seed >>> 0;
|
|
}
|
|
|
|
/** Current internal state, for serialising a game in progress. */
|
|
get state(): number {
|
|
return this._state;
|
|
}
|
|
|
|
/** Restores a generator to a previously captured state. */
|
|
static fromState(state: number): Rng {
|
|
return new Rng(state);
|
|
}
|
|
|
|
/** Uniform float in [0, 1). */
|
|
next(): number {
|
|
this._state = (this._state + 0x6d2b79f5) >>> 0;
|
|
let t = this._state;
|
|
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
}
|
|
|
|
/** Uniform integer in [0, bound). `bound` must be positive. */
|
|
nextInt(bound: number): number {
|
|
if (bound <= 0) {
|
|
throw new RangeError(`Rng.nextInt bound must be positive, got ${bound}`);
|
|
}
|
|
return Math.floor(this.next() * bound);
|
|
}
|
|
|
|
/** Uniformly picks one element. Throws on an empty array. */
|
|
pick<T>(items: readonly T[]): T {
|
|
if (items.length === 0) {
|
|
throw new RangeError('Rng.pick called on an empty array');
|
|
}
|
|
return items[this.nextInt(items.length)];
|
|
}
|
|
|
|
/** Fisher-Yates, in place. Returns the same array for convenience. */
|
|
shuffle<T>(items: T[]): T[] {
|
|
for (let i = items.length - 1; i > 0; i--) {
|
|
const j = this.nextInt(i + 1);
|
|
const tmp = items[i];
|
|
items[i] = items[j];
|
|
items[j] = tmp;
|
|
}
|
|
return items;
|
|
}
|
|
}
|