mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-18 06:41:27 +02:00
The instrument the design discussion said was missing. src/sim/ drives the actual
game engine (game.js — same dungeon gen, monster AI, movement as prod) with
headless bots to terminal outcomes, then measures what was only ever DECLARED:
- Per preset: escape/treasure/caught/stuck rates + completion-move/-time pctiles.
- MEASURED house-win-rate per network, integrating out the random block deadline
analytically: for D~Exp(mean=blockTime), P(house wins an escape at T)=1-exp(-T/BT),
so no Monte-Carlo over deadlines.
- Two bot skill models (omniscient beeline / explorer fog-of-war) as an upper band;
neither evades the monster, so true house-win is likely below both.
First findings (explorer, 200 runs, 320ms/move): presets UNDERSHOOT their declared
targets (casino 57% vs 70%, normal 47% vs 55% on WOW) and the edge swings ~15pts
across chains from IDENTICAL dungeons (casino 48% BTC .. 65% GRIN) — because
cryptoType never actually sizes the dungeon. Exactly the two gaps we predicted.
Files: src/sim/{pathfind,simBots,simulate}.js + test/sim_ultra.test.js (structural
guard, not a balance assertion). Run: node src/sim/simulate.js --help-ish flags.
Suite 59/422 green.
51 lines
2.0 KiB
JavaScript
51 lines
2.0 KiB
JavaScript
// Grid BFS used by the balance-sim bots. Kept dependency-free (no ROT) so the sim is a thin,
|
||
// auditable layer over the REAL game engine — it never reimplements game rules, only navigation.
|
||
|
||
// Breadth-first distance field from one or more source cells over a passability predicate.
|
||
// Returns a rows×cols Int32Array-of-arrays where unreachable = -1. 4-connected (the game moves
|
||
// in cardinal directions only).
|
||
function bfsField(cols, rows, passable, sources) {
|
||
const dist = [];
|
||
for (let y = 0; y < rows; y++) dist.push(new Array(cols).fill(-1));
|
||
const q = [];
|
||
for (const [sx, sy] of sources) {
|
||
if (sy >= 0 && sy < rows && sx >= 0 && sx < cols && dist[sy][sx] === -1) {
|
||
dist[sy][sx] = 0;
|
||
q.push([sx, sy]);
|
||
}
|
||
}
|
||
const DIRS = [[1, 0], [-1, 0], [0, 1], [0, -1]];
|
||
let head = 0;
|
||
while (head < q.length) {
|
||
const [x, y] = q[head++];
|
||
const d = dist[y][x];
|
||
for (const [dx, dy] of DIRS) {
|
||
const nx = x + dx, ny = y + dy;
|
||
if (nx < 0 || ny < 0 || nx >= cols || ny >= rows) continue;
|
||
if (dist[ny][nx] !== -1) continue;
|
||
if (!passable(nx, ny)) continue;
|
||
dist[ny][nx] = d + 1;
|
||
q.push([nx, ny]);
|
||
}
|
||
}
|
||
return dist;
|
||
}
|
||
|
||
// Given a distance field (to a target), return the cardinal step {dx,dy} that most reduces
|
||
// distance from (px,py), or null if there's no downhill neighbour (unreachable / already there).
|
||
function stepDownField(dist, px, py, cols, rows) {
|
||
const DIRS = [[1, 0], [-1, 0], [0, 1], [0, -1]];
|
||
const here = dist[py] && dist[py][px];
|
||
if (here == null || here < 0) return null;
|
||
let best = null, bestD = here;
|
||
for (const [dx, dy] of DIRS) {
|
||
const nx = px + dx, ny = py + dy;
|
||
if (nx < 0 || ny < 0 || nx >= cols || ny >= rows) continue;
|
||
const nd = dist[ny][nx];
|
||
if (nd >= 0 && nd < bestD) { bestD = nd; best = { dx, dy }; }
|
||
}
|
||
return best;
|
||
}
|
||
|
||
module.exports = { bfsField, stepDownField };
|