Capture surviving alone BG groups at positive remote board sources and zero replacement targets. Preserve returned armies, replacement, original leader death, revelation and card cycle. Native Face Dance stays immune; consume the publicly sourced parent before one original aftermath continuation. Validation: 16 affected files201/201, types/lint/build, five native Face Dance and GameTable SSR journeys, eight existing-profile response paths. Genuine 67/66-action sources earn original Ix47, native Face Dancers and actual battle. Both conversion outcomes preserve nine returned survivors and one leader death; actual BG city income is2/0. Correct a fixture's expected deck count43 to47 without changing cards or rules. Full combinations/modes remain unfinished. Not deployed; no saved-room/D1, reset or restart operation. Runtime fingerprint 78239a9def5dd70ae404d5a614a21ea09ea615dd2a6cde7d05760715416d7401; checkpoint prose/reference overlays are reviewed separately.
108 lines
6.6 KiB
TypeScript
108 lines
6.6 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { applyAction, normalizeAutomaticGame, viewGame, type Action, type Game } from '../game/engine';
|
|
import { isAdvisor, fighterCount } from '../game/advisors';
|
|
import { botActions } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import {
|
|
createAdvisorFaceDanceFlipFixture as fixture, actAdvisorFaceDanceFlip as act,
|
|
commitAdvisorFaceDanceFlip as commit, finishAdvisorFaceDanceFlip as finish,
|
|
advisorFaceDanceFlipPlayer as player, advisorFaceDanceFlipInventory as inventory,
|
|
advisorFaceDanceFlipTraitors as traitors,
|
|
} from './fixture-advisor-face-dance-flip';
|
|
|
|
function allow(g: Game) {
|
|
for (let count = 0; g.response?.automaticAdvisorFlip && count < 30; count++) {
|
|
const actor = g.players.find(p => !g.response!.passed.includes(p.id)); assert.ok(actor);
|
|
g = act(g, actor.id, { type: 'passResponse' });
|
|
}
|
|
assert.ok(!g.response?.automaticAdvisorFlip); return g;
|
|
}
|
|
function rejected(g: Game, actor: string, action: Action) {
|
|
const before = structuredClone(g); assert.throws(() => applyAction(g, actor, action)); assert.deepEqual(g, before);
|
|
}
|
|
const physical = (g: Game) => g.players.map(p => ({ id: p.id, forces: p.forces,
|
|
reserves: p.reserves, tanks: p.tanks, battleLosses: p.battleLosses, leaders: p.leaders }));
|
|
|
|
for (const zeroReplacement of [false, true]) for (const canceled of [false, true]) {
|
|
void test(`native ${zeroReplacement ? 'zero replacement' : 'remote source'} ${canceled ? 'prevention' : 'allowance'} preserves completed Face Dance and resumes aftermath once`, () => {
|
|
const f = fixture({ zeroReplacement }), before = structuredClone(f.game);
|
|
const pending = commit(f.game, f).game;
|
|
assert.deepEqual(f.game, before);
|
|
assert.equal(pending.phase, 6); assert.equal(pending.battle, null);
|
|
assert.equal(pending.decision, null); assert.equal(pending.pendingFaceDance, null);
|
|
const source = pending.response?.automaticAdvisorFlip; assert.ok(source?.producer === 'faceDance');
|
|
assert.equal(source.faceDance.battle.event, pending.lastBattleContext?.event);
|
|
assert.equal(source.faceDance.territory, f.battleTerritory);
|
|
assert.equal(source.faceDance.amount, zeroReplacement ? 0 : 9);
|
|
assert.deepEqual(source.faceDance.sources, f.sources);
|
|
assert.deepEqual(source.faceDance.returned, { [f.winner]: 9 });
|
|
assert.equal(player(pending, f.winner).reserves, player(before, f.winner).reserves + 9);
|
|
assert.equal(player(pending, f.winner).forces[`${f.battleTerritory}:10`], undefined);
|
|
const leader = player(pending, f.winner).leaders.find(l => l.id === f.winnerLeader)!;
|
|
assert.equal(leader.dead, true);
|
|
assert.equal(leader.deaths, player(before, f.winner).leaders.find(l => l.id === f.winnerLeader)!.deaths + 1);
|
|
assert.equal(player(pending, f.tleilaxu).faceDancers!.find(c => c.leader === f.winnerLeader)!.revealed, true);
|
|
assert.equal(player(pending, f.tleilaxu).forces[f.location], undefined);
|
|
assert.equal(player(pending, f.tleilaxu).forces['imperial_basin:10'], zeroReplacement ? 2 : 9);
|
|
assert.equal(fighterCount(player(pending, f.bg), f.territory), 0);
|
|
assert.deepEqual(pending.players.map(p => p.spice), before.players.map(p => p.spice), 'Face Dance grants no extra bounty');
|
|
assert.ok(viewGame(pending, f.holder).responseControls?.cancelCards.includes(f.karama));
|
|
assert.ok(!JSON.stringify(viewGame(pending, f.holder).response).includes(f.winnerLeader));
|
|
rejected(pending, f.tleilaxu, f.action);
|
|
const committed = physical(pending), bgSpice = player(pending, f.bg).spice;
|
|
let done = canceled
|
|
? act(pending, f.holder, { type: 'card', card: f.karama, mode: 'cancel' }) : allow(pending);
|
|
assert.equal(done.pendingAutomaticAdvisorFaceDance, undefined);
|
|
assert.equal(done.pendingAutomaticAdvisorFlips, undefined);
|
|
assert.deepEqual(physical(done), committed);
|
|
assert.equal(isAdvisor(player(done, f.bg), f.territory), canceled);
|
|
done = finish(done);
|
|
assert.equal(done.phase, 7);
|
|
assert.equal(player(done, f.bg).spice, bgSpice + (canceled ? 0 : 2));
|
|
assert.deepEqual(physical(done), committed);
|
|
for (let read = 0; read < 3; read++) {
|
|
done = normalizeAutomaticGame(done);
|
|
assert.deepEqual(physical(done), committed);
|
|
assert.equal(player(done, f.bg).spice, bgSpice + (canceled ? 0 : 2));
|
|
}
|
|
assert.equal(done.discard.filter(c => c.id === f.karama).length, canceled ? 1 : 0);
|
|
assert.deepEqual(inventory(done), f.originalInventory);
|
|
assert.deepEqual(traitors(done), f.originalTraitors);
|
|
});
|
|
}
|
|
|
|
void test('declining native Face Dance creates no conversion and keeps the original winner and unrevealed card', () => {
|
|
const f = fixture();
|
|
rejected(f.game, f.holder, { type: 'card', card: f.karama, mode: 'cancel' });
|
|
const declined = act(f.game, f.tleilaxu, { type: 'decision', reveal: false });
|
|
assert.equal(declined.pendingAutomaticAdvisorFaceDance, undefined);
|
|
assert.equal(player(declined, f.winner).forces[`${f.battleTerritory}:10`], 9);
|
|
assert.equal(player(declined, f.winner).leaders.find(l => l.id === f.winnerLeader)!.dead, false);
|
|
assert.equal(player(declined, f.tleilaxu).faceDancers!.find(c => c.leader === f.winnerLeader)!.revealed, false);
|
|
assert.equal(player(declined, f.tleilaxu).forces[f.location], 1);
|
|
const done = finish(declined);
|
|
assert.equal(isAdvisor(player(done, f.bg), f.territory), true);
|
|
assert.deepEqual(inventory(done), f.originalInventory);
|
|
});
|
|
|
|
void test('a changed completed Face Dance parent rejects before another card, force or leader is consumed', () => {
|
|
const f = fixture(), pending = commit(f.game, f).game;
|
|
pending.pendingAutomaticAdvisorFaceDance = { ...pending.pendingAutomaticAdvisorFaceDance!, amount: 8 };
|
|
rejected(pending, f.holder, { type: 'card', card: f.karama, mode: 'cancel' });
|
|
assert.ok(player(pending, f.holder).hand.some(c => c.id === f.karama));
|
|
assert.equal(player(pending, f.winner).leaders.find(l => l.id === f.winnerLeader)!.deaths, 1);
|
|
});
|
|
|
|
void test('all four existing profiles handle both Face Dance conversion causes legally', () => {
|
|
for (const zeroReplacement of [false, true]) for (const profile of DIFFICULTIES) {
|
|
const f = fixture({ zeroReplacement }), pending = commit(f.game, f).game, own = viewGame(pending, f.holder);
|
|
own.players.find(p => p.id === f.holder)!.bot = profile;
|
|
const actions = botActions(own); assert.ok(actions.length);
|
|
const done = finish(allow(act(pending, f.holder, actions[0])));
|
|
assert.equal(done.pendingAutomaticAdvisorFaceDance, undefined);
|
|
assert.deepEqual(physical(done), physical(pending));
|
|
assert.deepEqual(inventory(done), f.originalInventory);
|
|
}
|
|
});
|