Files
ark-stronghold-protocol/test/match/jessica-placement.test.js
T
sganggs 6bfc08270c 0.2.4-dev: round-29 community fixes and the official shared card pool
- Shared card pool and item stock as in the official mode (the owner's request, from the 路标档案馆 video and public notes): solo low-tier exceptions, co-op operators x2, shared item stock with special-item exceptions, normal / advanced occupancy 1 / 2, re-check on buy, 机变 exhaustion, release on consume / destroy / elimination; 整备 never exceeds stock; invariant held <= cap.
- 突袭: one taunt-first reselect right after a revive (the owner's rule); ordinary polls and redeploys keep the 0.2.3 rule; a revive on an enemy's tile stays. #475: online boss / hidden-boss Raid may land on the empty half (owner-limited scope).
- 浊心斯卡蒂 S1 x 坚守 no longer bounce redirected damage, and the two shares conserve the hit (idea of PR #464 by @HappyTanuki).
- Operator fixes adapted from PRs by @Xinglan233 (#459 哈洛德 S2 elemental trigger, #460 涤火杰西卡 shield on the four adjacent tiles, #461 家族徽章 first hit after stealth, #462 松果 S2 regrow per deploy); 克莱门莎 S2 splash 1.1 / 100% ATK and air auto-trigger (#467); 白面 copying 歌蕾蒂娅 / 余 skips the three-member condition (#473, the owner's video).
- 香槟炸弹 is not chosen by friendly auras / heals (PR #468 / #476 by @Siornya); 琳琅诗怀雅 coin balance (PR #450 by @IceCodeNew); Touch prep display (PR #451 by @YinqiMargatroid); lobby header (PR #471 by @YuriRestia); Japanese UI terms (PR #448 by @scutkane).
- AI-only boss countdown grace, render-failure cache / recovery without restarting a battle, device units kept on a same-field re-entry, DIY detail / skill previews, large-text result buttons.
- golden: battle families equal 0.2.3 except diy-135 / diy-136 (克莱门莎); all 18 release matches move through the shared pool, every flip named in docs/history/0.2.4.md §29.12; two coverage scenarios added.
2026-10-11 01:58:52 +08:00

134 lines
7.0 KiB
JavaScript

// Jessica's shield placement uses the real Match actions and the client's drag-preview mirror. The only fixture
// grant is the selected operator; its shield is generated by normal deployment. No battle/UI is simulated here.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { makeMatch } from './harness.js';
import { placementContext, canPlace, boardTargets } from '../../public/js/ui/gameLogic/placement.js';
import { planLayout } from '../../server/match/bot.js';
const SLOT = 'chess_char_5_diy1_a';
const JESCA = 'char_1034_jesca2';
const SHIELD = 'token_10032_jesca2_jckshd';
const board = (row, col) => ({ area: 'board', row, col });
const move = (h, uid, to, dir) => h.m.handle('p_0', { t: 'g.move', uid, to, ...(dir ? { dir } : {}) });
const stackOf = (ps, owner) => [...ps.hand, ...ps.temp].find((p) => p?.kind === 'token' && p.ownerUid === owner.uid);
const ctx = (h, ps) => placementContext({ priv: ps.privateView(), stage: h.m.stage, editable: true,
getChess: (id) => ps.gd.chess(id), getToken: (id) => ps.gd.token(id), backups: ps.gd.raw.backups });
function prep(t, { row = 10, col = 4, skillIndex = 0, uniEquipId = null } = {}) {
const h = makeMatch({ mode: 'solo', seed: 11, seats: [{ seat: 0, playerId: 'p_0', name: 'P0', isBot: false,
connected: true, diy: { [SLOT]: { charId: JESCA, skillIndex, uniEquipId } } }] }).start();
t.after(() => h.m.dispose());
h.toPrep(1);
assert.equal(h.m.stageId, 'act2autochess_m04');
const ps = h.ps('p_0');
const owner = ps.acquireChess(SLOT, { source: 'grant' });
assert.ok(owner);
assert.deepEqual(move(h, owner.uid, board(row, col)), { ok: true });
assert.equal(stackOf(ps, owner)?.id, SHIELD);
return { h, ps, owner };
}
test('Jessica: hand shield refuses diagonal and distant tiles, with unchanged state and matching drag highlights', (t) => {
const { h, ps, owner } = prep(t, { row: 9, col: 5 });
const shield = stackOf(ps, owner);
for (const [row, col] of [[9, 7], [10, 4], [11, 5], [12, 3]]) {
const before = structuredClone(ps.privateView());
assert.equal(canPlace(ctx(h, ps), shield.uid, board(row, col)).code, 'BAD_TILE', `client ${row},${col}`);
assert.equal(move(h, shield.uid, board(row, col)).error, 'BAD_TILE', `server ${row},${col}`);
assert.deepEqual(ps.privateView(), before, 'rejected placement changes no inventory or board state');
}
assert.deepEqual(boardTargets(ctx(h, ps), shield.uid).legal.sort(), [[9, 4], [9, 6], [10, 5]].sort());
h.invariants();
});
test('Jessica: the server rejects distant and diagonal placements even when the client sends them directly', (t) => {
const { h, ps, owner } = prep(t, { row: 9, col: 5 });
const shield = stackOf(ps, owner);
for (const [row, col] of [[9, 7], [10, 4]]) {
assert.equal(move(h, shield.uid, board(row, col)).error, 'BAD_TILE');
assert.equal(stackOf(ps, owner), shield);
assert.ok(!ps.board.has(`${row},${col}`));
}
h.invariants();
});
test('Jessica: all four orthogonal tiles are legal regardless of her facing, skill or selected module', (t) => {
for (const [skillIndex, uniEquipId] of [[0, null], [1, 'uniequip_002_jesca2'], [2, 'uniequip_003_jesca2']]) {
const { h, ps, owner } = prep(t, { skillIndex, uniEquipId });
for (const dir of ['UP', 'RIGHT', 'DOWN', 'LEFT']) {
assert.deepEqual(move(h, owner.uid, board(10, 4), dir), { ok: true });
for (const [row, col] of [[9, 4], [11, 4], [10, 3], [10, 5]]) {
const shield = stackOf(ps, owner);
assert.equal(canPlace(ctx(h, ps), shield.uid, board(row, col)).ok, true);
assert.deepEqual(move(h, shield.uid, board(row, col)), { ok: true });
assert.deepEqual(move(h, ps.board.get(`${row},${col}`).uid, { area: 'hand', idx: ps.hand.findIndex((p) => !p) }), { ok: true });
}
}
h.invariants();
}
});
test('Jessica: a deployed shield cannot move or be swapped to a distant tile; adjacent moves and owner swaps work', (t) => {
const { h, ps, owner } = prep(t);
assert.deepEqual(move(h, stackOf(ps, owner).uid, board(10, 5)), { ok: true });
const shield = ps.board.get('10,5');
for (const [row, col] of [[11, 5], [9, 7]]) {
assert.equal(canPlace(ctx(h, ps), shield.uid, board(row, col)).code, 'BAD_TILE');
assert.equal(move(h, shield.uid, board(row, col)).error, 'BAD_TILE');
assert.equal(ps.board.get('10,5'), shield);
}
const other = ps.acquireChess('chess_char_1_02_a', { source: 'grant' });
assert.deepEqual(move(h, other.uid, board(9, 7)), { ok: true });
for (const [uid, target] of [[shield.uid, board(9, 7)], [other.uid, board(10, 5)]]) {
assert.equal(canPlace(ctx(h, ps), uid, target).code, 'BAD_TILE');
assert.equal(move(h, uid, target).error, 'BAD_TILE');
}
assert.equal(canPlace(ctx(h, ps), shield.uid, board(11, 4)).ok, true);
assert.deepEqual(move(h, shield.uid, board(11, 4)), { ok: true });
assert.equal(canPlace(ctx(h, ps), shield.uid, board(10, 4)).ok, true);
assert.deepEqual(move(h, shield.uid, board(10, 4)), { ok: true });
assert.equal(ps.board.get('11,4'), owner);
assert.equal(ps.board.get('10,4'), shield);
h.invariants();
});
test('Jessica: owner reorientation keeps the shield; moving her returns it and withdrawal removes it', (t) => {
const { h, ps, owner } = prep(t);
assert.deepEqual(move(h, stackOf(ps, owner).uid, board(10, 5)), { ok: true });
const shield = ps.board.get('10,5');
assert.equal(canPlace(ctx(h, ps), shield.uid, board(10, 5)).action, 'orient');
assert.deepEqual(move(h, shield.uid, board(10, 5), 'LEFT'), { ok: true });
assert.deepEqual(move(h, owner.uid, board(10, 4), 'UP'), { ok: true });
assert.equal(ps.board.get('10,5'), shield);
assert.deepEqual(move(h, owner.uid, board(9, 4)), { ok: true });
assert.ok(!ps.board.has('10,5'));
const returned = stackOf(ps, owner);
assert.equal(returned.count, 1);
assert.equal(canPlace(ctx(h, ps), returned.uid, board(10, 5)).code, 'BAD_TILE');
assert.equal(move(h, returned.uid, board(10, 5)).error, 'BAD_TILE');
assert.deepEqual(move(h, returned.uid, board(9, 5)), { ok: true });
assert.deepEqual(move(h, owner.uid, { area: 'hand', idx: ps.hand.findIndex((p) => !p) }), { ok: true });
assert.ok(![...ps.board.values(), ...ps.hand, ...ps.temp].some((p) => p?.ownerUid === owner.uid));
h.invariants();
});
test('Jessica: bots plan her shield on an orthogonal tile', (t) => {
const { h, ps, owner } = prep(t);
const shield = stackOf(ps, owner);
const plan = planLayout(h.m, ps, [shield], undefined, { occupied: new Set(ps.board.keys()) });
assert.ok(['9,4', '11,4', '10,3', '10,5'].includes(plan.get(shield.uid)));
h.invariants();
});
test('other summons keep their own placement rules, including a medical drone far from its owner', (t) => {
const { h, ps } = prep(t);
const silence = ps.acquireChess('chess_char_2_02_a', { source: 'grant' });
assert.deepEqual(move(h, silence.uid, board(9, 3)), { ok: true });
const drone = stackOf(ps, silence);
assert.ok(drone);
assert.equal(canPlace(ctx(h, ps), drone.uid, board(12, 8)).ok, true);
assert.deepEqual(move(h, drone.uid, board(12, 8)), { ok: true });
h.invariants();
});