Implement engine core: board assembly, movement, turns, combat, victory

Pure deterministic game core in @wizwar/engine: seeded RNG (mulberry32,
state in GameState so seed+commands replays identically), sector
assembly with rotation, junction merging, and wraparound warps
(configurable pairings; the 2p diagram crosses its side openings),
movement (3 + one number card), geometric line of sight, deck building
from the verified card data (asserts 125/200 totals), and the
command-to-event reducer: setup with TRAP! redraw and die-roll first
player, punching (no combat round 1, no self-attack, once per turn),
damage/death with killer-takes-cards and forced discard, treasure
stealing with both victory conditions, pick-up-ends-turn, and
end-of-turn draw. Events carry full spatial detail for future replay
rendering; private card knowledge rides on visibleTo events with a
redaction helper. 21 tests passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-15 19:39:50 -04:00
co-authored by Claude Fable 5
parent 11209cdc5d
commit a8884592a4
9 changed files with 1428 additions and 31 deletions
+45
View File
@@ -0,0 +1,45 @@
// Deterministic RNG (mulberry32). The generator state lives inside GameState so
// that replaying the same commands from the same seed reproduces the game
// exactly — the foundation for async play, spectating, and replays.
export interface RngState {
/** Current 32-bit mulberry32 state; advances by one per value drawn. */
readonly a: number;
}
export function createRng(seed: number): RngState {
return { a: seed >>> 0 };
}
/** Draw the next float in [0, 1). Returns the value and the advanced state. */
export function nextFloat(state: RngState): [number, RngState] {
const a = (state.a + 0x6d2b79f5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
const value = ((t ^ (t >>> 14)) >>> 0) / 4294967296;
return [value, { a: a >>> 0 }];
}
/** Draw an integer in [0, n). */
export function nextInt(state: RngState, n: number): [number, RngState] {
const [f, next] = nextFloat(state);
return [Math.floor(f * n), next];
}
/** Roll the Wiz-War die (numbered 1-4). */
export function rollDie(state: RngState): [number, RngState] {
const [i, next] = nextInt(state, 4);
return [i + 1, next];
}
/** Fisher-Yates shuffle. Returns a new array and the advanced state. */
export function shuffle<T>(state: RngState, items: readonly T[]): [T[], RngState] {
const arr = [...items];
let rng = state;
for (let i = arr.length - 1; i > 0; i--) {
const [j, next] = nextInt(rng, i + 1);
rng = next;
[arr[i], arr[j]] = [arr[j]!, arr[i]!];
}
return [arr, rng];
}