Files
Dune/tests/advisor-gamont-flip-engine.test.ts
Quintine 035be51915 Resolve advisor conversion after Gamont before final victory
Commit original Gamont use/return or empty revelation before its separate mandatory-conversion counter. Preserve private fuel provenance beside a public phase8 receipt, retain legacy phase5 sources and defer the genuine final chooser through both direct and Semuta cost completion. Consume the parent once; ordinary Mentat gains no chooser and victory waits for original confirmation. Existing provisional continuously-alone duration remains.

Validation: 18 files277/277, types/lint/build, five native Gamont/response/GameTable SSR journeys and eight existing-profile paths. Genuine52/56-action public Advanced sources earn a real third-stronghold win versus prevented next-turn outcome. Empty revelation remains pure-source proof; full combinations/remaining producers, browser/deployed and final assurance stay open. No saved-room/D1/restart/deployment operation or AI refinement.
2026-10-24 20:16:05 +11:00

125 lines
6.6 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, normalizeAutomaticGame, viewGame, type Game, type Action } from '../game/engine';
import { fighterCount, isAdvisor } from '../game/advisors';
import { strongholdProgress } from '../game/victory-progress';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import {
createAdvisorGamontFlipFixture as fixture,
actAdvisorGamontFlip as act,
commitAdvisorGamontFlip as commit,
advisorGamontFlipPlayer as player,
advisorGamontFlipInventory as inventory,
passAdvisorGamontFlip as pass,
finishAdvisorGamontFlip as finish,
} from './fixture-advisor-gamont-flip';
function rejected(g: Game, actor: string, action: Action) {
const before = structuredClone(g); assert.throws(() => applyAction(g, actor, action));
assert.deepEqual(g, before);
}
const holds = (g: Game, id: string) => strongholdProgress(g).progress.find(p => p.player === id)!.strongholds.length;
for (const final of [false, true]) for (const canceled of [false, true]) {
void test(`native ${final ? 'final' : 'ordinary'} Gamont ${canceled ? 'prevention' : 'allowance'} keeps the return and changes the genuine victory outcome`, () => {
const f = fixture({ final }), before = structuredClone(f.game);
assert.equal(holds(before, f.bg), 2);
const pending = commit(f.game, f).game;
assert.deepEqual(f.game, before);
assert.equal(pending.response?.kind, 'advisorFlip');
const source = pending.response?.automaticAdvisorFlip;
assert.ok(source?.producer === 'gamont');
assert.equal(source.gamont.mentat, final);
assert.equal(source.gamont.returned, true);
assert.equal(pending.decision, null);
assert.equal(pending.status, 'playing');
assert.deepEqual(pending.winner, []);
assert.equal(player(pending, f.holder).forces[f.from], undefined);
assert.equal(player(pending, f.holder).reserves, player(before, f.holder).reserves + 1);
assert.equal(player(pending, f.holder).tanks, player(before, f.holder).tanks);
assert.equal(pending.discard.filter(c => c.id === f.card).length, 1);
assert.equal(fighterCount(player(pending, f.bg), f.territory), 0);
assert.equal(holds(pending, f.bg), 2);
rejected(pending, f.choam, { type: 'decision', done: true });
assert.ok(viewGame(pending, f.holder).responseControls?.cancelCards.includes(f.karama));
const publicResponse = JSON.stringify(viewGame(pending, f.holder).response);
assert.ok(!publicResponse.includes(f.card), 'The public conversion never copies the spent fuel card');
assert.equal(pending.pendingAutomaticAdvisorGamont?.card, f.card);
const returned = structuredClone(player(pending, f.holder));
let done = canceled
? act(pending, f.holder, { type: 'card', card: f.karama, mode: 'cancel' })
: pass(pending);
assert.equal(player(done, f.holder).reserves, returned.reserves);
assert.equal(player(done, f.holder).tanks, returned.tanks);
assert.equal(player(done, f.holder).forces[f.from], undefined);
assert.equal(done.discard.filter(c => c.id === f.card).length, 1);
assert.equal(done.pendingAutomaticAdvisorGamont, undefined);
assert.equal(done.pendingAutomaticAdvisorFlips, undefined);
assert.equal(isAdvisor(player(done, f.bg), f.territory), canceled);
assert.equal(holds(done, f.bg), canceled ? 2 : 3);
assert.equal(done.status, 'playing');
assert.deepEqual(done.winner, []);
if (final) assert.deepEqual(done.decision, { kind: 'choamMentat', player: f.choam });
else assert.equal(done.decision, null);
for (let read = 0; read < 3; read++) {
done = normalizeAutomaticGame(done);
assert.equal(done.status, 'playing');
assert.equal(player(done, f.holder).reserves, returned.reserves);
}
assert.deepEqual(inventory(done), f.originalInventory);
const finished = finish(done);
if (canceled) {
assert.equal(finished.status, 'playing');
assert.equal(finished.turn, before.turn + 1);
assert.deepEqual(finished.winner, []);
} else {
assert.equal(finished.status, 'finished');
assert.deepEqual(finished.winner, [f.bg]);
assert.equal(finished.turn, before.turn);
}
assert.equal(finished.discard.filter(c => c.id === f.card).length, 1);
});
}
void test('preventing the original Gamont effect leaves the last outsider and final chooser intact without a conversion parent', () => {
const f = fixture(), declared = act(f.game, f.choam, f.action);
assert.equal(declared.response?.kind, 'choamWorthless');
const done = act(declared, f.holder, { type: 'card', card: f.karama, mode: 'cancel' });
assert.equal(player(done, f.holder).forces[f.from], 1);
assert.equal(player(done, f.holder).reserves, player(f.game, f.holder).reserves);
assert.ok(player(done, f.choam).hand.some(c => c.id === f.card));
assert.equal(done.pendingAutomaticAdvisorGamont, undefined);
assert.equal(done.pendingAutomaticAdvisorFlips, undefined);
assert.deepEqual(done.decision, { kind: 'choamMentat', player: f.choam });
assert.equal(holds(done, f.bg), 2);
const next = finish(done);
assert.equal(next.status, 'playing'); assert.equal(next.turn, f.game.turn + 1);
});
void test('a changed Gamont parent rejects before spending a second card or returning a second force', () => {
const f = fixture(), pending = commit(f.game, f).game;
pending.pendingAutomaticAdvisorGamont = { ...pending.pendingAutomaticAdvisorGamont!,
receipt: { ...pending.pendingAutomaticAdvisorGamont!.receipt, target: f.bg } };
rejected(pending, f.holder, { type: 'card', card: f.karama, mode: 'cancel' });
assert.equal(pending.discard.filter(c => c.id === f.card).length, 1);
assert.ok(player(pending, f.holder).hand.some(c => c.id === f.karama));
});
void test('the existing four profiles resolve ordinary and final conversion responses legally before victory', () => {
for (const final of [false, true]) for (const profile of DIFFICULTIES) {
const f = fixture({ final }), pending = commit(f.game, f).game;
const 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 = pass(act(pending, f.holder, actions[0]));
assert.equal(done.pendingAutomaticAdvisorGamont, undefined);
assert.equal(done.status, 'playing');
assert.equal(player(done, f.holder).reserves, player(f.game, f.holder).reserves + 1);
const next = finish(done);
assert.ok(next.status === 'finished' || next.turn === f.game.turn + 1);
assert.equal(next.discard.filter(c => c.id === f.card).length, 1);
assert.deepEqual(inventory(next), f.originalInventory);
}
});