M0: план разработки, техническая структура, каркас проекта
- 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>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "e9145215-b0c0-46a4-83b9-caf393a20cc1",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
Reference in New Issue
Block a user