Tests: test/sim/feedback5-raid-own-board.test.js — the pair field (the right player's member onto the left telin's board tile (2,3), the left player's onto the mirrored right (2,17), the hand rows closed) and the two-helper 联防 (each helper onto the other's half; 乌尔比安 beside a teammate stays on his own) fail on the parent; the solo boss field's hand rows and the lone helper unchanged. Docs: docs/history/0.2.1.md (new, §26; §26.1 cites the owner's decision and PRTS 卫戍协议/帮助 §联防阶段), the DESIGN index row and versions line, a pointer from §25.18.1, docs/SIM.md (onFieldBoard; onOwnBoard now 乌尔比安's only).
127 lines
9.8 KiB
JavaScript
127 lines
9.8 KiB
JavaScript
// test/sim/feedback5-raid-own-board.test.js — where a 突袭 member may land (DESIGN §25.18.1, §26.1).
|
||
// Community reports of 2026-10-06, items 40 and 16.3 (fixed in 0.2.0):
|
||
// 「…新约能天使在开了突袭之后蹦到了下方原本用来临时放置溢出的干员和道具的那一栏高台的最左侧位置」 — 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.
|
||
// 0.2.0 let raidTile take only tiles of the member's own board (Battle.onOwnBoard: the board region, rows 9–12 / cols
|
||
// 2–10, mapped onto the field). The owner's decision of 2026-10-07 (0.2.1) opens a teammate's half when both halves are
|
||
// taken — the two-helper 联防 field and a Final Assault / Hidden Core pair field: raidTile takes the tiles of
|
||
// Battle.onFieldBoard (the board of any player of the field). The lone helper's empty half, a solo boss field's other
|
||
// half and the hand rows stay closed; 乌尔比安's S3 【移动】 keeps his own board.
|
||
// 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 TANK = 'chess_char_1_02_a'; // 角峰: a teammate's piece (no 突袭)
|
||
const ULPIA = 'chess_char_5_05_a';
|
||
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: o.bonds ?? BONDS, units: o.units ?? [] });
|
||
const unitAt = (row, col, uid = 1, chessId = ID) => ({ uid, kind: 'chess', chessId, row, col });
|
||
/** A teammate with one 角峰 and no 突袭 (its half is taken). */
|
||
const mate = (playerId, s, o = {}) => seat(playerId, s, { ...o, bonds: {}, units: [unitAt(12, 8, 9, TANK)] });
|
||
|
||
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`);
|
||
// no partner: the field's boards are its own (the right half stays closed)
|
||
for (let r = 0; r <= 5; r++) for (let c = 0; c <= 20; c++) assert.equal(h.b.onFieldBoard(r, c), h.b.onOwnBoard(u.player, r, c), `(${r},${c})`);
|
||
// 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: a member lands on its partner\'s half — the right player\'s on the left, the left player\'s on the mirrored right (the owner\'s decision of 2026-10-07) — never on the hand rows', () => {
|
||
// the enemy on the left telin (1,3): the right player's member (board (12,3) → field (5,17)) takes the partner's board
|
||
// tile next to it (until 0.2.1 it stayed home: no tile of its own half reaches the telin)
|
||
const { h, mine, homes } = jump({ kind: 'boss', stageId: 'act1autochess_m01', players: [mate('p1', 0), seat('p2', 1, { side: 'R', units: [unitAt(12, 3)] })], enemyPos: [1, 3] });
|
||
const u = mine[0];
|
||
assert.equal(u.ownerId, 'p2');
|
||
assert.deepEqual(homes[0], [5, 17]);
|
||
assert.deepEqual([u.tileR, u.tileC], [2, 3], 'the partner\'s board tile next to the telin');
|
||
const partner = h.b.getPlayer('p1');
|
||
assert.ok(!h.b.onOwnBoard(u.player, 2, 3) && h.b.onOwnBoard(partner, 2, 3) && h.b.onFieldBoard(2, 3));
|
||
// both halves are the field's boards, the hand / 临时整备区 rows of either side never
|
||
for (let c = 0; c <= 20; c++) for (const r of [0, 1]) assert.equal(h.b.onFieldBoard(r, c), false, `(${r},${c})`);
|
||
for (let r = 2; r <= 5; r++) for (let c = 2; c <= 18; c++) assert.equal(h.b.onFieldBoard(r, c), true, `(${r},${c})`);
|
||
// the mirror: the left player's member (field (5,3)) lands on the right player's half, next to the right telin
|
||
const left = jump({ kind: 'boss', stageId: 'act1autochess_m01', players: [seat('p1', 0, { units: [unitAt(12, 3)] }), mate('p2', 1, { side: 'R' })], enemyPos: [1, 17] });
|
||
assert.deepEqual([left.mine[0].tileR, left.mine[0].tileC], [2, 17], 'the mirrored right half');
|
||
assert.ok(!left.h.b.onOwnBoard(left.mine[0].player, 2, 17) && left.h.b.onOwnBoard(left.h.b.getPlayer('p2'), 2, 17));
|
||
// an enemy only the partner's hand row could reach: it stays home and does not hop
|
||
const far = jump({ kind: 'boss', stageId: 'act1autochess_m01', players: [mate('p1', 0), seat('p2', 1, { side: 'R', units: [unitAt(12, 3)] })], enemyPos: [0, 3] });
|
||
assert.deepEqual([far.mine[0].tileR, far.mine[0].tileC], far.homes[0]);
|
||
});
|
||
|
||
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`);
|
||
// a lone helper: the right half is nobody's board, not even the field's
|
||
for (let r = 9; r <= 12; r++) for (let c = 11; c <= 20; c++) assert.equal(h.b.onFieldBoard(r, c), false, `(${r},${c})`);
|
||
}
|
||
// 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): a member lands on its teammate\'s half (the owner\'s decision of 2026-10-07); 乌尔比安\'s S3 【移动】 still keeps to his own', () => {
|
||
// the enemy on the right helper's half: the left helper's member jumps onto it — the enemy's own road tile, the
|
||
// nearest that covers it (this map's road takes ranged units too); until 0.2.1 it stayed home
|
||
const players = [seat('L', 0, { units: [unitAt(12, 3, 1)] }), mate('R', 1, { colOffset: 8 })];
|
||
const { h, mine, homes } = jump({ kind: 'unite', stageId: 'act1autochess_escaped_multi', players, enemyPos: [10, 15] });
|
||
const u = mine[0];
|
||
assert.deepEqual(homes[0], [12, 3]);
|
||
assert.deepEqual([u.tileR, u.tileC], [10, 15], 'the right helper\'s half');
|
||
assert.ok(!h.b.onOwnBoard(u.player, 10, 15) && h.b.onOwnBoard(h.b.getPlayer('R'), 10, 15) && h.b.onFieldBoard(10, 15));
|
||
// the right-hand helper (board col 9 → field col 17) lands on the left helper's half the same way
|
||
const right = jump({ kind: 'unite', stageId: 'act1autochess_escaped_multi', players: [mate('L', 0), seat('R', 1, { colOffset: 8, units: [unitAt(12, 9, 1)] })], enemyPos: [10, 5] });
|
||
assert.deepEqual(right.homes[0], [12, 17]);
|
||
assert.deepEqual([right.mine[0].tileR, right.mine[0].tileC], [10, 5], 'the left helper\'s half');
|
||
// 乌尔比安 beside a teammate: an anchor that meets no enemy flies 6 tiles ahead to (10,13), the teammate's half — a
|
||
// field board but not his: he stays where he stands (DESIGN §25.17.2, a separate community report)
|
||
const ul = makeBattle({
|
||
kind: 'unite', stageId: 'act1autochess_escaped_multi', seed: 5, autoFinish: false, timeLimit: 120, defs,
|
||
players: [seat('L', 0, { bonds: {}, units: [unitAt(10, 7, 1, ULPIA)] }), mate('R', 1, { colOffset: 8 })],
|
||
});
|
||
ul.step();
|
||
const a = ul.unit(ULPIA);
|
||
a.skill.gainSp(999, 'test');
|
||
if (!a.skill.active) assert.ok(a.skill.activate('test'), 'S3 cast');
|
||
ul.run(0.2);
|
||
checkInvariants(ul.b);
|
||
assert.ok(ul.b.onFieldBoard(10, 13) && !ul.b.onOwnBoard(a.player, 10, 13));
|
||
assert.deepEqual([a.tileR, a.tileC], [10, 7], 'he stays on his own half');
|
||
});
|