Files
ark-stronghold-protocol/test/match/feedback5-unite-map.test.js
T
sganggs 8a43624935 feedback5 WE: 联防 is played on its own map, the escaped template level (GitHub #41, item 3) — act2autochess constData escapedBattleTemplateMapSinglePlayer / MultiPlayer = level_act1autochess_escaped_single / _multi name the level of the 联防 battle, and its map is the field: two road halves (cols 3–9 and 11–17, rows 9–12) joined at col 10, no water, crates or devices (until 0.1.3 the round's stage, opened to both halves). tools/build-data.mjs builds both into data/stages.json after the 11 battle stages (kind 'unite', helpers 1 / 2, weight 0, inactive, no modes, the name 联防阵地(N名玩家) the remake's own label [ASSUMED]; validation ties config.unite.templates to them); regenerated offline, a JSON compare shows only the two added records. unite.js uniteStageId picks the map of the helper count (escaped_single's routes enter at col 10, escaped_multi's at col 18 through (9,10)); match/unitePhase.js fields the 联防 battle on it (the round's stage only with data that lacks it); each helper on its prep tiles, the first of two shifted 8 columns onto the right half as before (the official halves; player_map_lr_offset 8). Client: screens/game.js draws the shown field's own map and restores the board's on leaving it (a 2-line change in a file of the R5 split; render/app.js enterBattle already loads m.field stageId); the dev mock's 联防 follows. The 联防 BGM (PR #110) is unchanged. Tests: test/match/feedback5-unite-map.test.js (both combat modes, 1 and 2 helpers, degraded data), test/data.test.js, test/render/tiles.test.js. Docs: DATA §12, META §4, DESIGN §3, PLAYING §5. Golden: fields unite-1 / unite-2 (the battle's stage → the 联防 map) and the 6 co-op match scenarios with a 联防 round (coop2-NORMAL-3, coop2-HARD-4, coop3-ABYSS-5, coop4-FUNNY-6, coop4-ABYSS-7, coop2-NORMAL-8-serverrun); roster / bonds unchanged (tools/golden.mjs leaves the 联防 maps out of its stage list)
2026-10-05 18:17:08 +08:00

130 lines
7.5 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.
// GitHub #41 (item 3): the 联防 field was the round's own stage — its water, crates and devices included — opened to both
// halves. Official: act2autochess constData escapedBattleTemplateMapSinglePlayer / MultiPlayer = level_act1autochess_
// escaped_single / _multi, the level of the 联防 battle and its map (two road halves joined at col 10). One helper →
// escaped_single (enemies enter at col 10), two helpers → escaped_multi (enemies enter at col 18 and pass (9,10)); the
// helpers' pieces stand on their prep tiles ("按休整期位置部署在场"), the first of two shifted 8 columns onto the right half
// ("率先迎敌(即位于右侧阵地)"). data/stages.json holds both maps (kind 'unite'); server/match/unite.js uniteStageId.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { GEO, PHASE } from '../../shared/constants.js';
import { Battle } from '../../server/sim/Battle.js';
import { createBattleFromSpec } from '../../server/sim/spec.js';
import { DataSource } from '../../server/sim/simdata.js';
import { uniteStageId } from '../../server/match/unite.js';
import { FakeBattle } from './fakeBattle.js';
import { DATA, makeMatch, give, chessOfTier, legalTileFor, checkInvariants } from './harness.js';
/** A real battle that only ends by its time limit (the check follows the enemies, whatever the helpers do). */
class NoFinish extends Battle {
constructor(o) { super({ ...o, autoFinish: false }); }
}
/**
* Co-op on 战场#01 (its row 9 is fenced off at cols 5–7: "##Err###rrSrr###rrS##"): p_0 leaks 3 enemies, the other
* players are perfect — 1 helper with 2 humans, 2 helpers with 3. Each helper fields one ranged operator in its corner.
*/
function scenario({ humans, clientCombat }) {
const h = makeMatch({
mode: 'coop', humans, seed: 4101 + humans, fake: true, clientCombat,
script: (b) => (b.kind === 'normal' ? { leaks: { p_0: 3 } } : {}),
}).start();
const m = h.m;
h.toPrep(1);
h.setStage('act1autochess_m01');
const ranged = chessOfTier(1, (c) => c.position === 'RANGED').filter((x) => m.pool.has(x));
const helpers = [];
for (let i = 1; i < humans; i++) {
const ps = h.ps(`p_${i}`);
const id = ranged[i];
helpers.push({ ps, piece: give(m, ps, id, 'board', legalTileFor(m, ps, id)) });
}
h.drive(() => m.phase === PHASE.UNITE);
return { h, m, helpers };
}
/** The 联防 field's spec / options as the match built them, and a real battle over them. */
function uniteField(m, clientCombat) {
if (clientCombat) {
const f = m.fields[0];
return { opts: f.spec, battle: createBattleFromSpec(f.spec, new DataSource(DATA, null), { BattleClass: NoFinish, recordEvents: false }) };
}
const u = FakeBattle.instances.find((b) => b.kind === 'unite');
return { opts: u.opts, battle: new NoFinish({ ...u.opts, data: m.ds, logger: { warn() {}, error() {}, info() {}, debug() {} } }) };
}
for (const clientCombat of [true, false]) {
test(`联防 with 1 helper (${clientCombat ? 'client-side combat' : 'server-run'}): escaped_single, not the round's stage — enemies enter at col 10 and walk row 9`, () => {
const { m, helpers } = scenario({ humans: 2, clientCombat });
assert.deepEqual(m.unitePlan.helpers.map((p) => p.playerId), ['p_1']);
const { opts, battle: b } = uniteField(m, clientCombat);
assert.equal(opts.stageId, 'act1autochess_escaped_single');
assert.equal(uniteStageId(m.gd, 1), 'act1autochess_escaped_single');
assert.deepEqual(opts.rect, GEO.UNITE_RECT, 'the whole 19×21 map\'s field rows (both halves are road on it)');
assert.equal(b.stage.id, 'act1autochess_escaped_single');
assert.deepEqual(b.stage.devices, [], 'no crates or devices of 战场#01');
// the map: row 9 is road from the gate (9,10) to the objective (9,2); on 战场#01 cols 5–7 are fenced off
for (let c = 3; c <= 9; c++) assert.ok(b.grid.groundPassable(9, c), `(9,${c}) is road`);
assert.equal(m.stage.rows[9].slice(5, 8), '###', '战场#01 itself has no ground there');
for (const c of [19, 20]) assert.ok(!b.grid.groundPassable(9, c), `(9,${c}) is no ground`);
// the routes of escaped_single: every one starts at col 10
assert.ok(opts.routes.every((r) => (r.start ?? [r.startPosition?.row, r.startPosition?.col])[1] === 10));
// the helper's piece stands on its prep tile
const { ps, piece } = helpers[0];
const [r, c] = [...ps.board.entries()].find(([, p]) => p === piece)[0].split(',').map(Number);
b.step();
const u = b.allyUnits.find((x) => x.uid === piece.uid && x.ownerId === 'p_1');
assert.deepEqual([u.tileR, u.tileC], [r, c]);
// a walker crosses the tiles 战场#01 fences off
const crossed = new Set();
while (b.time < 60 && !b.finished) {
b.step();
for (const e of b.enemies) if (e.alive && e.motion !== 'FLY') crossed.add(`${Math.round(e.y)},${Math.round(e.x)}`);
}
assert.ok(['9,5', '9,6', '9,7'].some((k) => crossed.has(k)), `a walker on row 9 cols 5–7 (${[...crossed].sort().join(' ')})`);
assert.equal(b.errorCount || 0, 0);
checkInvariants(m);
m.dispose();
});
}
test('联防 with 2 helpers: escaped_multi — the first helper on the right half (col + 8), enemies enter at col 18 through (9,10); the client draws the field\'s map', () => {
const { h, m, helpers } = scenario({ humans: 3, clientCombat: false });
const order = m.unitePlan.helpers.map((p) => p.playerId);
assert.equal(order.length, 2);
const { opts, battle: b } = uniteField(m, false);
assert.equal(opts.stageId, 'act1autochess_escaped_multi');
assert.equal(b.stage.id, 'act1autochess_escaped_multi');
assert.deepEqual(opts.players.map((p) => [p.playerId, p.colOffset]), [[order[0], 8], [order[1], 0]]);
assert.ok(opts.routes.every((r) => r.start[1] === 18), 'every route enters at col 18');
assert.ok(opts.routes.filter((r) => r.motion === 'WALK').every((r) => r.checkpoints.some(([rr, cc]) => rr === 9 && cc === 10)), 'walkers pass (9,10)');
b.step();
for (const { ps, piece } of helpers) {
const [r, c] = [...ps.board.entries()].find(([, p]) => p === piece)[0].split(',').map(Number);
const u = b.allyUnits.find((x) => x.uid === piece.uid && x.ownerId === ps.playerId);
const off = ps.playerId === order[0] ? 8 : 0;
assert.deepEqual([u.tileR, u.tileC], [r, c + off], `${ps.playerId}: its prep tile${off ? ' on the right half' : ''}`);
assert.equal(b.stage.rows[r][c + off], 'r', 'a road tile of the 联防 map');
}
// what a watching browser receives: the m.field of the 联防 carries the map it is drawn on
m.handle('p_0', { t: 'g.watch', fieldId: 'u' });
const meta = h.lastTo('p_0', 'm.field');
assert.equal(meta && meta.stageId, 'act1autochess_escaped_multi');
assert.equal(m.stageId, 'act1autochess_m01', 'the match stage (m.public stageId, the boards) stays the round\'s');
checkInvariants(m);
m.dispose();
});
test('degraded data without the 联防 maps: the field keeps the round\'s stage', () => {
const stages = Object.fromEntries(Object.entries(DATA.stages).filter(([, s]) => s.kind !== 'unite'));
const h = makeMatch({ mode: 'coop', humans: 2, seed: 4199, fake: true, data: { ...DATA, stages }, script: (b) => (b.kind === 'normal' ? { leaks: { p_0: 2 } } : {}) }).start();
const m = h.m;
h.toPrep(1);
assert.equal(uniteStageId(m.gd, 1), null);
const ps = h.ps('p_1');
const id = chessOfTier(1, (c) => c.position === 'RANGED').find((x) => m.pool.has(x));
give(m, ps, id, 'board', legalTileFor(m, ps, id));
h.drive(() => m.phase === PHASE.UNITE);
assert.equal(FakeBattle.instances.find((b) => b.kind === 'unite').opts.stageId, m.stageId);
m.dispose();
});