// Official route shapes (community report after 0.1.0, D5: on 战场#04 活性源石 the lower-gate enemies walked two tiles // straight left of the red gate and then straight up, instead of crossing diagonally from the 4th row to the 3rd as in // the official game). grid.js keeps the official flow field (research 08 §3.1) unless the blockable-ground preference // route of user playtest #2 (0.1.0's) crosses strictly fewer non-blockable tiles (floor, gates) — a segment that only // brushes a floor tile's corner does not cross it. Audit: every stage (active or not) × gate × field against the pure // official algorithm; the routes that still differ are listed, each avoiding floor the official route walks over. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { Grid, segmentTiles } from '../../server/sim/grid.js'; import { getDefaultSource, hasGeneratedData } from '../../server/sim/simdata.js'; import { GEO } from '../../shared/constants.js'; import { makeBattle, chessRec, enemyRec } from '../helpers/battleHarness.js'; const REAL = { skip: !hasGeneratedData() && 'no generated data' }; const COLS = 21; const M04 = 'act1autochess_m04'; const RECT = { normal: GEO.NORMAL_RECT, unite: GEO.UNITE_RECT, boss: GEO.BOSS_RECT }; /** Grid of a real stage with its match-start devices; `pure` = the official algorithm (every tile counted blockable). */ function stageGrid(id, rect, pure = false) { const st = getDefaultSource().getStage(id); if (!st) return null; const g = new Grid(st, rect); for (const d of st.raw.devices) { if (!d.active || !d.pos) continue; if (d.role === 'crate') g.setObstacle(d.pos[0], d.pos[1], true, 'crate'); if (d.role === 'platform' || d.role === 'mound') g.setObstacle(d.pos[0], d.pos[1], true); } if (pure) g.unblockable.fill(0); return g; } const str = (wp) => (wp ? wp.map((p) => p.join(',')).join(' ') : 'null'); test('D5 战场#04 活性源石: the lower-gate route is the official diagonal from row 9 into row 10 (normal and 联防 fields)', REAL, () => { const official = [[9, 10], [10, 7], [10, 4], [9, 4], [9, 2]]; assert.deepEqual(stageGrid(M04, GEO.NORMAL_RECT, true).waypoints(9, 10, 9, 2), official, 'the pure official algorithm'); assert.deepEqual(stageGrid(M04, GEO.NORMAL_RECT).waypoints(9, 10, 9, 2), official, 'normal field'); assert.deepEqual(stageGrid(M04, GEO.UNITE_RECT).waypoints(9, 10, 9, 2), official, '联防 field, own lower gate'); // the segment (9,10) → (10,7) only brushes the corner of the floor tile (10,9): it crosses no non-blockable tile assert.deepEqual(segmentTiles([9, 10], [10, 7]), [[9, 10], [9, 9], [10, 8], [10, 7]]); }); test('D5 real battle (act1 m04, round-1 template act1autochess_01): the 源石虫 leave the gate diagonally, never along row 9 to col 8', REAL, () => { const h = makeBattle({ stageId: M04, waveTemplate: 'act1autochess_01', units: [], autoFinish: false }); let id = null, defId = null; const tiles = []; let atCol9 = null; h.b.on('tick', () => { const e = h.b.enemies.find((u) => (id == null ? u.motion === 'WALK' : u.id === id)); if (!e) return; id = e.id; defId = e.defId; const k = `${Math.round(e.y)},${Math.round(e.x)}`; if (tiles[tiles.length - 1] !== k) tiles.push(k); if (atCol9 == null && e.x <= 9) atCol9 = e.y; }); h.run(30); assert.equal(defId, 'enemy_1007_slime', 'the round-1 源石虫 of the lower gate'); assert.deepEqual(tiles.slice(0, 5), ['9,10', '9,9', '10,8', '10,7', '10,6'], `tiles: ${tiles.join(' ')}`); assert.ok(!tiles.includes('9,8'), `never on (9,8): ${tiles.join(' ')}`); // on the official segment (9,10) → (10,7) the walker is a third of a row up when it passes the col-9 centre line assert.ok(Math.abs(atCol9 - (9 + 1 / 3)) < 0.02, `row ${atCol9} at col 9 (straight along row 9 would be 9.00)`); }); // Routes that differ from the official one (ours, as waypoints): each crosses fewer non-blockable tiles — user playtest // #2's 战场#01 col-9 floor lane (its 联防 partner routes pass the same exit), the boss exits (2,10) of 战场#01 / #02 // that would cut across the arena's central floor, 战场#02's 联防 partner lower gate (floor (12,17)) and the inactive // 战场#07 (上半)'s 联防 partner upper gate (row 12 through (12,9) vs row 9). Every other route of every stage is official. const DEVIATIONS = { 'act1autochess_m01 normal 9,10>9,2': '9,10 9,8 12,8 12,4 9,4 9,2', 'act1autochess_m01 unite 9,10>9,2': '9,10 9,8 12,8 12,4 9,4 9,2', 'act1autochess_m01 unite 9,18>9,2': '9,18 9,16 12,16 12,12 9,12 9,8 12,8 12,4 9,4 9,2', 'act1autochess_m01 unite 12,18>9,2': '12,18 12,12 9,12 9,8 12,8 12,4 9,4 9,2', 'act1autochess_m01 boss 2,10>1,3': '2,10 2,8 5,8 5,4 2,4 2,3 1,3', 'act1autochess_m01 boss 2,10>2,2': '2,10 2,8 5,8 5,4 2,4 2,2', 'act1autochess_m01 boss 2,10>1,17': '2,10 2,12 5,12 5,16 2,16 2,17 1,17', 'act1autochess_m01 boss 2,10>2,18': '2,10 2,12 5,12 5,16 2,16 2,18', 'act1autochess_m02 unite 9,18>9,2': '9,18 9,17 11,17 12,12 12,3 9,3 9,2', 'act1autochess_m02 boss 2,10>1,3': '2,10 2,9 4,9 4,6 2,6 2,3 1,3', 'act1autochess_m02 boss 2,10>2,2': '2,10 2,9 4,9 4,6 2,6 2,2', 'act1autochess_m02 boss 2,10>1,17': '2,10 2,11 4,11 4,14 2,14 2,17 1,17', 'act1autochess_m02 boss 2,10>2,18': '2,10 2,11 4,11 4,14 2,14 2,18', 'act1autochess_m07 unite 12,18>9,2': '12,18 12,11 9,11 9,2', // the inactive 战场#05 (上半): its 深水区 refuses deployment (player report #3 after 0.1.0) and the sim's Grid does not // model the 特制水上平台 canoes over it (deploy map only, server/sim/grid.js [ASSUMED, weight 0]), so its water counts as // non-blockable and these routes take the floor lanes beside it — one non-blockable tile fewer than the official ones 'act1autochess_m05 normal 12,10>9,2': '12,10 12,9 9,9 9,2', 'act1autochess_m05 unite 12,10>9,2': '12,10 12,9 9,9 9,2', 'act1autochess_m05 unite 12,18>9,2': '12,18 12,17 9,17 9,2', 'act1autochess_m05 boss 5,10>1,3': '5,10 5,9 2,9 2,3 1,3', 'act1autochess_m05 boss 5,10>2,2': '5,10 5,9 2,9 2,2', 'act1autochess_m05 boss 5,10>1,17': '5,10 5,11 2,11 2,17 1,17', 'act1autochess_m05 boss 5,10>2,18': '5,10 5,11 2,11 2,18', }; test('audit: every route of every stage (11, active or not) × gate × field is the official one, except the listed floor-avoiding ones', REAL, () => { const stages = ['act1autochess_m01', 'act1autochess_m02', 'act1autochess_m03', 'act1autochess_m04', 'act1autochess_m05', 'act1autochess_m06', 'act1autochess_m07', 'act2autochess_m01', 'act2autochess_m02', 'act2autochess_m03', 'act2autochess_m04']; const legs = [ ['normal', [9, 10], [9, 2]], ['normal', [12, 10], [9, 2]], ['unite', [9, 10], [9, 2]], ['unite', [12, 10], [9, 2]], ['unite', [9, 18], [9, 2]], ['unite', [12, 18], [9, 2]], ...[[5, 10], [2, 10]].flatMap((s) => [[1, 3], [2, 2], [1, 17], [2, 18]].map((e) => ['boss', s, e])), ]; const floor = (g, wp) => { const out = []; for (let i = 1; i < wp.length; i++) for (const [r, c] of segmentTiles(wp[i - 1], wp[i]).slice(1)) if (g.unblockable[r * COLS + c]) out.push(`${r},${c}`); return out; }; const seen = new Set(); let official = 0; for (const sid of stages) { for (const [kind, s, e] of legs) { const g = stageGrid(sid, RECT[kind]), p = stageGrid(sid, RECT[kind], true); assert.ok(g, `${sid} in data/stages.json`); const ours = g.waypoints(s[0], s[1], e[0], e[1]), off = p.waypoints(s[0], s[1], e[0], e[1]); const key = `${sid} ${kind} ${s}>${e}`; assert.ok(ours && off, `${key} reachable`); assert.equal(g.flowField(e[0], e[1]).dist[s[0] * COLS + s[1]], p.flowField(e[0], e[1]).dist[s[0] * COLS + s[1]], `${key}: official length`); if (str(ours) === str(off)) { assert.ok(!(key in DEVIATIONS), `${key} is official now: drop it from DEVIATIONS`); official++; continue; } seen.add(key); assert.equal(str(ours), DEVIATIONS[key], `${key} differs from the official ${str(off)}`); const fo = floor(g, ours), ff = floor(g, off); assert.ok(fo.length < ff.length, `${key}: crosses ${fo.join(' ')}, the official route ${ff.join(' ')}`); } } assert.equal(seen.size, Object.keys(DEVIATIONS).length, 'every listed deviation still exists'); assert.equal(official, stages.length * legs.length - seen.size); }); test('D5: an operator on (9,9), (9,8) or (10,8) blocks the lower-gate enemy on the official diagonal', REAL, () => { const guard = chessRec({ id: 't_guard', profession: 'WARRIOR', stats: { atk: 0, blockCnt: 3, maxHp: 1e6 }, skill: null }); const walker = enemyRec({ key: 'enemy_walker', hp: 1e6, speed: 1 }); const route = { motion: 'WALK', start: [9, 10], end: [9, 2], checkpoints: [] }; for (const [r, c] of [[9, 9], [9, 8], [10, 8]]) { const h = makeBattle({ stageId: M04, defs: { chess: { t_guard: guard }, enemies: { enemy_walker: walker } }, units: [{ chessId: 't_guard', row: r, col: c }], enemies: [{ key: 'enemy_walker', route }], content: 'none', autoFinish: false, timeLimit: 60 }); const u = h.unit('t_guard'); assert.ok(h.runUntil(() => h.enemies()[0]?.blockedBy === u, 30), `blocked by the operator on (${r},${c})`); } }); test('战场#02 Final Assault: the (2,10) exit routes mirror each other and an operator beside the exit on (2,9) / (2,11) blocks them', REAL, () => { // the preference route is 0.1.0's (a diagonal step's corner tiles count in its line of sight); only the choice between // it and the official route counts exact crossings — otherwise the right half slipped diagonally past (2,11) const g = stageGrid('act1autochess_m02', GEO.BOSS_RECT); const mirror = (wp) => wp.map(([r, c]) => [r, 20 - c]); assert.deepEqual(g.waypoints(2, 10, 1, 17), mirror(g.waypoints(2, 10, 1, 3))); assert.deepEqual(g.waypoints(2, 10, 2, 18), mirror(g.waypoints(2, 10, 2, 2))); const guard = chessRec({ id: 't_guard', profession: 'WARRIOR', stats: { atk: 0, blockCnt: 3, maxHp: 1e6 }, skill: null }); const walker = enemyRec({ key: 'enemy_walker', hp: 1e6, speed: 1 }); for (const [tile, end] of [[[2, 9], [1, 3]], [[2, 9], [2, 2]], [[2, 11], [1, 17]], [[2, 11], [2, 18]]]) { const h = makeBattle({ stageId: 'act1autochess_m02', kind: 'boss', defs: { chess: { t_guard: guard }, enemies: { enemy_walker: walker } }, units: [{ chessId: 't_guard', row: tile[0], col: tile[1] }], enemies: [{ key: 'enemy_walker', route: { motion: 'WALK', start: [2, 10], end, checkpoints: [] } }], content: 'none', autoFinish: false, timeLimit: 60 }); const u = h.unit('t_guard'); assert.ok(h.runUntil(() => h.enemies()[0]?.blockedBy === u, 20), `the enemy (2,10) → (${end}) is blocked on (${tile})`); assert.ok(u.deployed && u.tileR === tile[0] && u.tileC === tile[1], `operator on (${tile})`); } }); test('equal floor never bends the official route: one crate on any tile of an active stage leaves every gate route official or crossing strictly fewer non-blockable tiles', REAL, () => { const stages = ['act1autochess_m01', 'act1autochess_m02', 'act1autochess_m03', 'act1autochess_m04', 'act2autochess_m01', 'act2autochess_m02', 'act2autochess_m03', 'act2autochess_m04']; const floor = (g, wp) => { let n = 0; for (let i = 1; i < wp.length; i++) for (const [r, c] of segmentTiles(wp[i - 1], wp[i]).slice(1)) n += g.unblockable[r * COLS + c]; return n; }; const R = GEO.NORMAL_RECT; let n = 0; for (const sid of stages) { for (let r = R.r0; r <= R.r1; r++) { for (let c = R.c0; c <= R.c1; c++) { const g = stageGrid(sid, R), p = stageGrid(sid, R, true); if (!g.walkable(r, c) || g.obstacle[r * COLS + c]) continue; g.setObstacle(r, c, true, 'crate'); p.setObstacle(r, c, true, 'crate'); for (const s of [[9, 10], [12, 10]]) { const ours = g.waypoints(s[0], s[1], 9, 2), off = p.waypoints(s[0], s[1], 9, 2); if (!ours || str(ours) === str(off)) continue; n++; assert.ok(floor(g, ours) < floor(g, off), `${sid} crate (${r},${c}) from (${s}): ${str(ours)} vs the official ${str(off)}`); } } } } assert.ok(n > 0, 'some layouts keep a floor-avoiding deviation'); // the case that used to slip through: a crate on (10,6) of 战场#04 — official (9,10) → (9,9) → (11,7), as much floor as // the L via (9,8), so the official one stays const g = stageGrid(M04, R); g.setObstacle(10, 6, true, 'crate'); assert.equal(str(g.waypoints(9, 10, 9, 2)), '9,10 9,9 11,7 11,4 9,4 9,2'); });