Files
wownerogue/src/sim/pathfind.js
T
jwinterm fd4327a54f feat(sim): headless balance-calibration harness (drives the REAL engine)
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.
2026-07-11 19:43:12 -04:00

51 lines
2.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 };