134 lines
7.0 KiB
JavaScript
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();
|
|
});
|