// 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(); });