feedback5 CB4: a 突袭 member lands only on its own player's board — never the boss field's hand / 临时整备区 rows, never the other half of a 联防 or boss field (community reports of 2026-10-06, items 40 and 16.3)

Cause: raidTile took any standable tile of the battle rect; BOSS_RECT holds rows 0-1 (tile_achand, buildable high
ground) and UNITE_RECT both halves of the 联防 map. Battle.onOwnBoard(ps, r, c) maps the board region (rows 9-12,
cols 2-10) onto the field as mapTile maps the pieces; raidTile takes only those tiles.

Golden: 8 scenarios, each with members that landed on the partner's half on the parent (checked instrumented):
fields boss-boss_2/3/4/5/6-pair, hidden-boss_8/10-pair; matches coop2-ABYSS-10-boosted (ai_0 on the right half).
Tests: test/sim/feedback5-raid-own-board.test.js (4, fail on the parent). Docs: DESIGN §25.18.1, docs/SIM.md.
This commit is contained in:
sganggs committed 2026-10-07 01:36:53 +08:00
1 parent 30d9a41c51
commit 219ab31a3d
7 files changed
+535 -352

No files matched your search

+87
View File
@@ -0,0 +1,87 @@
// test/sim/feedback5-raid-own-board.test.js — 突袭 lands only on its player's own board (community reports of
// 2026-10-06, items 40 and 16.3; DESIGN §25.18):
// 「…新约能天使在开了突袭之后蹦到了下方原本用来临时放置溢出的干员和道具的那一栏高台的最左侧位置」 — on a boss field the
// battle rect (BOSS_RECT rows 0–5) holds the hand row (0) and the 临时整备区 row (1), high ground buildable ALL, so a
// ranged member could land there;
// 「单人联防时突袭也可以跑到对面的场地上」 — on the 联防 map of one helper (escaped_single) the right half (cols 11–18)
// is in the rect but is nobody's board.
// raidTile now takes only tiles of Battle.onOwnBoard: the board region (rows 9–12, cols 2–10) mapped onto the field.
// Run: node --test test/sim/feedback5-raid-own-board.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
import { getDefaultSource } from '../../server/sim/simdata.js';
const ID = 'chess_char_6_13_a'; // 新约能天使 (RANGED), given the 突袭 bond as the 转职球 / 突袭手雷 would
const raw = getDefaultSource().rawChess(ID);
const raider = { ...raw, bonds: [...raw.bonds, 'raidShip'] };
const BONDS = { raidShip: { count: 2, active: true, tier: 1, layers: 0 } };
const dummy = enemyRec({ key: 'dummy', hp: 1e9, speed: 0, mass: 0 });
const defs = { enemies: { dummy }, chess: { [ID]: raider } };
function jump({ kind, stageId, players, enemyPos, seconds = 11 }) {
const h = makeBattle({ kind, stageId, autoFinish: false, timeLimit: 60, defs, players });
h.step();
const mine = h.b.allyUnits.filter((u) => u.defId === ID);
const homes = mine.map((u) => [u.tileR, u.tileC]);
h.spawn('dummy', { pos: enemyPos });
h.run(seconds);
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
return { h, mine, homes };
}
const seat = (playerId, s, o = {}) => ({ playerId, seat: s, side: o.side ?? 'L', colOffset: o.colOffset ?? 0, bonds: BONDS, units: o.units ?? [] });
const unitAt = (row, col, uid = 1) => ({ uid, kind: 'chess', chessId: ID, row, col });
test('boss field (solo 最终攻势): a ranged 突袭 member never lands on the hand row 0 or the 临时整备区 row 1', () => {
// 战场#01: the enemy on the telin (1,3); before the fix the member landed on the hand tile (0,3)
const { h, mine } = jump({ kind: 'boss', stageId: 'act1autochess_m01', players: [seat('p1', 0, { units: [unitAt(12, 3)] })], enemyPos: [1, 3] });
const u = mine[0];
assert.deepEqual([u.tileR, u.tileC], [2, 3], 'the board tile next to it, not (0,3)');
assert.ok(h.b.onOwnBoard(u.player, u.tileR, u.tileC));
// every row-0 / row-1 tile is off the board, every board tile (rows 2–5, cols 2–10) is on it
for (let c = 0; c <= 20; c++) for (const r of [0, 1]) assert.equal(h.b.onOwnBoard(u.player, r, c), false, `(${r},${c})`);
for (let r = 2; r <= 5; r++) for (let c = 2; c <= 10; c++) assert.equal(h.b.onOwnBoard(u.player, r, c), true, `(${r},${c})`);
for (let r = 2; r <= 5; r++) for (let c = 11; c <= 20; c++) assert.equal(h.b.onOwnBoard(u.player, r, c), false, `(${r},${c}) is the right half`);
// an enemy only the temp row could reach: it stays home and does not hop
const far = jump({ kind: 'boss', stageId: 'act1autochess_m01', players: [seat('p1', 0, { units: [unitAt(12, 3)] })], enemyPos: [0, 9] });
assert.deepEqual([far.mine[0].tileR, far.mine[0].tileC], far.homes[0]);
});
test('boss field pair: the mirrored right player lands on its own half only', () => {
const { h, mine } = jump({
kind: 'boss', stageId: 'act1autochess_m01',
players: [seat('p1', 0), seat('p2', 1, { side: 'R', units: [unitAt(12, 3)] })], enemyPos: [3, 9],
});
const u = mine[0];
assert.equal(u.ownerId, 'p2');
// the enemy stands on the partner's half next to the middle: the member takes a tile of its own half that reaches it
// (before the fix: (2,9), the partner's road)
assert.ok(u.tileR >= 2 && u.tileC >= 10, `landed on (${u.tileR},${u.tileC})`);
assert.ok(h.b.onOwnBoard(u.player, u.tileR, u.tileC));
assert.equal(h.b.onOwnBoard(u.player, 3, 5), false, 'the left half is the partner\'s');
assert.equal(h.b.onOwnBoard(u.player, 3, 15), true);
});
test('联防 with one helper (escaped_single): 突袭 does not jump onto the right half', () => {
for (const pos of [[10, 15], [11, 17], [9, 12]]) {
const { h, mine, homes } = jump({ kind: 'unite', stageId: 'act1autochess_escaped_single', players: [seat('H', 0, { units: [unitAt(12, 9)] })], enemyPos: pos });
const u = mine[0];
assert.ok(u.tileC <= 10, `enemy ${pos}: landed on (${u.tileR},${u.tileC})`);
assert.ok(h.b.onOwnBoard(u.player, u.tileR, u.tileC), `enemy ${pos}`);
if (pos[1] >= 13) assert.deepEqual([u.tileR, u.tileC], homes[0], `enemy ${pos}: out of reach from the left half, it stays`);
}
// an enemy the left half can reach: it still jumps (onto the left half)
const { mine } = jump({ kind: 'unite', stageId: 'act1autochess_escaped_single', players: [seat('H', 0, { units: [unitAt(12, 3)] })], enemyPos: [9, 9] });
assert.ok(mine[0].tileC <= 10 && mine[0].tileC >= 7, `landed on (${mine[0].tileR},${mine[0].tileC})`);
});
test('联防 with two helpers (escaped_multi): each helper lands on its own half', () => {
const players = [seat('L', 0, { units: [unitAt(12, 3, 1)] }), seat('R', 1, { colOffset: 8, units: [unitAt(12, 9, 2)] })];
const { h, mine } = jump({ kind: 'unite', stageId: 'act1autochess_escaped_multi', players, enemyPos: [10, 15] });
const right = mine.find((u) => u.ownerId === 'R'), left = mine.find((u) => u.ownerId === 'L');
assert.ok(right.tileC >= 11, `the right helper lands on its half: (${right.tileR},${right.tileC})`);
assert.ok(h.b.onOwnBoard(right.player, right.tileR, right.tileC));
assert.ok(left.tileC <= 10, `the left helper stays on its half: (${left.tileR},${left.tileC})`);
});