feedback6 R21A: a 突袭 member may land on a teammate's half when both halves of the field are taken — the two-helper 联防 field and the Final Assault / Hidden Core pair field (the owner's decision of 2026-10-07): Battle.onFieldBoard (onOwnBoard of any player of the battle) read by raidTile only; a lone 联防 helper's empty half, a solo boss field's other half and the hand / 临时整备区 rows stay closed, every other landing rule kept, 乌尔比安's S3 【移动】 keeps onOwnBoard; golden: 8 moved, each parting from the parent at a member's first landing on its teammate's half (checked tick by tick with the parent instrumented) — fields boss-boss_2 / 3 / 4 / 5 / 6-pair and hidden-boss_8 / 10-pair (the right player's 缄默德克萨斯 / 伊内丝 / 仇白 / 耀骑士临光 on the left player's half, 1–7 landings per battle, the first 0.3–39.8 s in), matches coop4-ABYSS-7 (round 6's two-helper 联防: ai_2's 斯卡蒂 on ai_1's half at 52 s; only ai_1 / ai_2 dmgDealt move, 8263 between them); unite-2 unchanged (its two landings stay on their own half)

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).
This commit is contained in:
sganggs committed 2026-10-07 17:09:39 +08:00
1 parent 40cfe2d3d3
commit cfd3c67281
11 files changed
+507 -394

No files matched your search

+3 -2
View File
@@ -117,8 +117,9 @@ test('战术家 援军 (伺夜): the tactical point is the tile in FRONT along t
test('突袭 (raidShip): the landing tile beside the enemy turns with the member direction (equal candidates)', () => {
// a diagonal-only range: two equally near landing tiles cover the enemy — the tie resolves in the member's frame. The
// enemy stands inside the player's own board (rows 9–12, cols 2–10): a member lands only there since 0.2.0
// (Battle.onOwnBoard, DESIGN §25.18), so all four diagonal tiles around (10, 6) are candidates in every direction
// enemy stands inside the player's own board (rows 9–12, cols 2–10): a member lands only on a board of its field since
// 0.2.0 (Battle.onFieldBoard — on a one-player field its own; DESIGN §25.18, §26.1), so all four diagonal tiles around
// (10, 6) are candidates in every direction
const ER = 10, EC = 6;
const rec = chessRec({ id: 'r_diag', bonds: ['raidShip'], profession: 'WARRIOR', skill: null, rangeGrid: [[1, 1], [-1, 1]] });
const land = (dir) => {
+62 -23
View File
@@ -1,11 +1,15 @@
// 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):
// 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.
// raidTile now takes only tiles of Battle.onOwnBoard: the board region (rows 9–12, cols 2–10) mapped onto the field.
// 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';
@@ -14,6 +18,8 @@ import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.
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 } };
@@ -31,8 +37,10 @@ function jump({ kind, stageId, players, enemyPos, seconds = 11 }) {
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 });
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)
@@ -44,24 +52,33 @@ test('boss field (solo 最终攻势): a ranged 突袭 member never lands on the
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: 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],
});
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');
// 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);
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', () => {
@@ -71,17 +88,39 @@ test('联防 with one helper (escaped_single): 突袭 does not jump onto the rig
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): 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})`);
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');
});