Check Advanced victory after Mentat actions
Verify and publish checkpoint container / container (push) Successful in 10m42s

This commit is contained in:
Quintine committed 2026-10-29 09:15:34 +11:00
1 parent c608d6fea6
commit 64706cc7e0
12 files changed
+339 -27

No files matched your search

@@ -0,0 +1,97 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, type Game } from '../game/engine';
import { territory } from '../game/board';
import { createMoritaniStrongholdsFixture } from './fixture-moritani-strongholds';
/** This inherited fixture uses real Advanced Moritani/Guild/Emperor setup and
* native phase actions through the opening Moritani placement decision, with
* the Stronghold Cards module. Only physical board groups are staged after
* that opening to expose a rare three-stronghold Guild victory. The prevention
* case also transfers one original Karama to Guild's hand without duplication.
* It is not a naturally earned full game or module-combination certification. */
function position() {
const fixture = createMoritaniStrongholdsFixture();
const game = structuredClone(fixture.beforePlacement);
assert.equal(game.phase, 8); assert.equal(game.decision?.kind, 'moritaniPlacement');
for (const player of game.players) {
player.reserves += Object.values(player.forces).reduce((sum, count) => sum + count, 0);
player.forces = {};
if (player.elites) {
player.elites.reserves += Object.values(player.elites.forces).reduce((sum, count) => sum + count, 0);
player.elites.forces = {};
}
}
const winner = game.players.find(player => player.faction === 'guild')!;
for (const name of ['arrakeen', 'carthag', 'sietch_tabr'])
winner.forces[`${name}:${territory(name).sectors[0]}`] = 1;
winner.reserves -= 3;
const owner = game.players.find(player => player.faction === 'moritani')!;
return { game, winner: winner.id, owner: owner.id };
}
function pending(game: Game) {
assert.equal(game.phase, 8); assert.equal(game.status, 'playing');
assert.deepEqual(game.winner, []); assert.ok(!game.victoryContinuation);
}
function finish(game: Game, winner: string) {
const turn = game.turn;
for (const player of game.players) game = applyAction(game, player.id, { type: 'ready' });
assert.equal(game.turn, turn); assert.equal(game.status, 'finished');
assert.deepEqual(game.winner, [winner]);
assert.equal(game.strongholdCards?.claimedTurn, turn);
}
function passPlacement(game: Game) {
while (game.response) {
const actor = game.players.find(player => !game.response!.passed.includes(player.id));
assert.ok(actor);
game = applyAction(game, actor.id, { type: 'passResponse' });
}
return game;
}
function stageKarama(game: Game, id: string) {
const player = game.players.find(seat => seat.id === id)!;
const held = player.hand.find(card => card.effect === 'karama');
if (held) return held.id;
const deckIndex = game.deck.findIndex(card => card.effect === 'karama');
let card = deckIndex >= 0 ? game.deck.splice(deckIndex, 1)[0] : undefined;
if (!card) {
const donor = game.players.find(seat => seat.id !== id && seat.hand.some(candidate => candidate.effect === 'karama'));
assert.ok(donor, 'One original Karama is still in the unplayed supply');
card = donor.hand.splice(donor.hand.findIndex(candidate => candidate.effect === 'karama'), 1)[0];
}
if (player.hand.length >= 4) game.deck.push(player.hand.pop()!);
player.hand.push(card);
return card.id;
}
void test('Advanced Moritani declining placement leaves the winner pending until Mentat closes', () => {
const f = position();
const game = applyAction(f.game, f.owner, { type: 'decision', decline: true });
pending(game); assert.equal(game.decision, null);
assert.equal(game.moritaniTerror!.placementTurn, game.turn);
finish(game, f.winner);
});
void test('Advanced Moritani placement completes before the later victory check', () => {
const f = position();
const token = f.game.moritaniTerror!.tokens.find(candidate => candidate.status === 'available')!;
let game = applyAction(f.game, f.owner, { type: 'decision', token: token.id, territory: 'tueks_sietch' });
game = passPlacement(game);
pending(game); assert.equal(game.moritaniTerror!.placementTurn, game.turn);
assert.equal(game.moritaniTerror!.tokens.find(candidate => candidate.id === token.id)?.location, 'tueks_sietch');
finish(game, f.winner);
});
void test('Advanced Moritani placement canceled by Karama still leaves victory until Mentat closes', () => {
const f = position(), card = stageKarama(f.game, f.winner);
const token = f.game.moritaniTerror!.tokens.find(candidate => candidate.status === 'available')!;
const original = structuredClone(f.game.moritaniTerror!.tokens);
let game = applyAction(f.game, f.owner, { type: 'decision', token: token.id, territory: 'tueks_sietch' });
assert.equal(game.response?.kind, 'moritaniPlacement');
game = applyAction(game, f.winner, { type: 'card', mode: 'cancel', card });
pending(game); assert.equal(game.moritaniTerror!.placementTurn, game.turn);
assert.deepEqual(game.moritaniTerror!.tokens, original);
assert.ok(game.discard.some(candidate => candidate.id === card));
finish(game, f.winner);
});
+118
View File
@@ -0,0 +1,118 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, viewGame, type Game } from '../game/engine';
import { territory } from '../game/board';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { createTechTokens } from '../game/tech-tokens';
import { bindTruthBribe, createTruthBribeFixture, truthBribePlayer } from './fixture-truth-bribe';
/** Original Advanced Harkonnen/Atreides/Emperor setup and native actions reach
* Collection. Only the conserved force locations are staged to expose a rare
* victory board; the agreement case also stages a complete physical Tech set,
* and the final-turn case stages turn ten. This is not a naturally earned
* late-game board, a save/recovery test, or full module acceptance. */
function position(options: { holds?: number; simultaneous?: boolean; finalTurn?: boolean } = {}) {
const source = createTruthBribeFixture({ advanced: true, phase: 7 });
const game = structuredClone(source.game);
for (const player of game.players) {
player.reserves += Object.values(player.forces).reduce((sum, count) => sum + count, 0);
player.forces = {};
if (player.elites) {
player.elites.reserves += Object.values(player.elites.forces).reduce((sum, count) => sum + count, 0);
player.elites.forces = {};
}
}
const assign = (id: string, names: string[]) => {
const player = truthBribePlayer(game, id);
for (const name of names) player.forces[`${name}:${territory(name).sectors[0]}`] = 1;
player.reserves -= names.length;
};
const holds = options.holds ?? 3;
assign('atreides', ['arrakeen', 'carthag', 'sietch_tabr'].slice(0, holds));
if (options.simultaneous) {
assign('emperor', ['tueks_sietch', 'habbanya_ridge_sietch']);
game.techTokens = createTechTokens();
for (const token of Object.values(game.techTokens)) token.owner = 'emperor';
}
if (options.finalTurn) game.turn = 10;
return { source, game };
}
function enterMentat(initial: Game) {
let game = initial;
for (const player of game.players) game = applyAction(game, player.id, { type: 'ready' });
assert.equal(game.phase, 8);
assert.equal(game.status, 'playing');
assert.deepEqual(game.winner, []);
assert.equal(game.victoryContinuation, undefined);
return game;
}
function finishMentat(initial: Game) {
let game = initial;
for (const player of game.players) game = applyAction(game, player.id, { type: 'ready' });
return game;
}
void test('Advanced winner waits through a real Truthtrance and its required Mentat payment', () => {
const f = position();
let game = enterMentat(f.game);
const bound = bindTruthBribe(f.source, 'yes', 3, f.source.recipient, game);
game = bound.game;
assert.throws(() => applyAction(game, f.source.payer, { type: 'ready' }), /Truthtrance|bribe|payment/i);
const guide = viewGame(game, f.source.payer).bribePromiseGuide;
assert.ok(guide);
const payerSpice = truthBribePlayer(game, f.source.payer).spice;
game = applyAction(game, f.source.payer, guide.action);
assert.equal(truthBribePlayer(game, f.source.payer).spice, payerSpice - 3);
assert.equal(truthBribePlayer(game, f.source.recipient).bribes, 3);
game = finishMentat(game);
assert.equal(game.status, 'finished');
assert.deepEqual(game.winner, ['atreides']);
});
void test('Advanced Mentat with no winner advances once only after every player is ready', () => {
let game = enterMentat(position({ holds: 2 }).game);
const turn = game.turn;
for (const player of game.players.slice(0, -1)) {
game = applyAction(game, player.id, { type: 'ready' });
assert.equal(game.phase, 8); assert.equal(game.turn, turn);
}
game = applyAction(game, game.players.at(-1)!.id, { type: 'ready' });
assert.equal(game.status, 'playing'); assert.equal(game.turn, turn + 1); assert.equal(game.phase, 0);
assert.deepEqual(game.winner, []);
});
void test('Advanced final-turn fallback is checked after Mentat opportunities', () => {
const game = enterMentat(position({ holds: 2, finalTurn: true }).game);
const settled = finishMentat(game);
assert.equal(settled.turn, 10); assert.equal(settled.status, 'finished');
assert.deepEqual(settled.winner, ['atreides']);
});
void test('Advanced simultaneous qualifiers vote after Mentat and a unanimous agreement advances once', () => {
const game = finishMentat(enterMentat(position({ simultaneous: true }).game));
const offer = game.victoryContinuation;
assert.ok(offer); assert.equal(offer.resume, 'advance');
assert.deepEqual(new Set(offer.voters), new Set(['atreides', 'emperor']));
let next = applyAction(game, offer.voters[0], { type: 'victoryContinuation', event: offer.event, mode: 'continue', extra: 1 });
for (const voter of offer.voters.slice(1))
next = applyAction(next, voter, { type: 'victoryContinuation', event: offer.event, mode: 'accept' });
assert.equal(next.status, 'playing'); assert.equal(next.turn, game.turn + 1); assert.equal(next.phase, 0);
assert.equal(next.victoryExtraStrongholds, 1); assert.ok(!next.victoryContinuation);
});
void test('all four minimal bot profiles can finish Advanced Mentat before victory', () => {
const initial = enterMentat(position().game);
for (const difficulty of DIFFICULTIES) {
let game = structuredClone(initial);
for (const player of game.players) {
const view = viewGame(game, player.id);
view.players.find(seat => seat.id === player.id)!.bot = difficulty;
const action = botActions(view).find(candidate => candidate.type === 'ready');
assert.ok(action, `${difficulty} can finish ${player.id}'s Mentat opportunity`);
game = applyAction(game, player.id, action);
}
assert.equal(game.status, 'finished'); assert.deepEqual(game.winner, ['atreides']);
}
});
@@ -34,6 +34,12 @@ function actual(g: Game) {
let result = structuredClone(g);
result.phase = 7;
for (const p of result.players) result = applyAction(result, p.id, { type: 'ready' });
if (result.advanced) {
assert.equal(result.phase, 8);
assert.equal(result.status, 'playing');
assert.ok(!result.victoryContinuation);
for (const p of result.players) result = applyAction(result, p.id, { type: 'ready' });
}
return result;
}
@@ -39,6 +39,12 @@ function originalMentat(game: Game) {
assert.equal(game.phase, 8);
return game;
}
function finishMentat(game: Game) {
assert.equal(game.phase, 8);
for (const player of game.players) if (!game.ready.includes(player.id))
game = applyAction(JSON.parse(JSON.stringify(game)), player.id, { type: 'ready' });
return game;
}
void test('actual original Mentat awards the high Ecaz two-native-world alliance win with one joint stronghold and no ordinary point inflation', () => {
const fixture = ecazHomeworldFixture(true);
@@ -59,11 +65,19 @@ void test('actual original Mentat awards the high Ecaz two-native-world alliance
const ready = botActions(view).find(action => action.type === 'ready') ?? { type: 'ready' as const };
result = applyAction(result, player.id, ready);
}
assert.equal(result.phase, 8); assert.equal(result.status, 'playing');
for (const player of result.players) {
const view = viewGame(result, player.id); view.players.find(seat => seat.id === player.id)!.bot = difficulty;
const ready = botActions(view).find(action => action.type === 'ready') ?? { type: 'ready' as const };
result = applyAction(result, player.id, ready);
}
assert.equal(result.status, 'finished');
assert.deepEqual(new Set(result.winner), new Set(['ec', 'al']));
assert.deepEqual(biddingPhysicalIds(result), cards);
}
game = originalMentat(game);
assert.equal(game.status, 'playing'); assert.deepEqual(game.winner, []);
game = finishMentat(game);
assert.equal(game.status, 'finished'); assert.deepEqual(new Set(game.winner), new Set(['ec', 'al']));
assert.deepEqual(biddingPhysicalIds(game), cards);
assert.deepEqual(game.players.map(player => ({ id: player.id, forces: player.forces, reserves: player.reserves, tanks: player.tanks })), forces);
@@ -77,6 +91,8 @@ void test('native population6 does not award this win despite the same actual jo
assert.equal(viewGame(game, 'ec').ecazHomeworldVictory!.high, false);
game = originalMentat(game);
assert.equal(game.status, 'playing'); assert.deepEqual(game.winner, []);
game = finishMentat(game);
assert.equal(game.status, 'playing'); assert.deepEqual(game.winner, []);
});
for (const predicted of ['ecaz', 'guild'] as const) void test(`actual BG prediction of ${predicted} replaces the printed Ecaz alliance win without changing public progress`, () => {
@@ -85,6 +101,8 @@ for (const predicted of ['ecaz', 'guild'] as const) void test(`actual BG predict
ecazSeat(game, 'observer').prediction = { faction: predicted, turn: game.turn };
assert.equal(viewGame(game, 'al').ecazHomeworldVictory!.qualifies, progress.qualifies);
game = originalMentat(game);
assert.equal(game.status, 'playing'); assert.deepEqual(game.winner, []);
game = finishMentat(game);
assert.deepEqual(game.winner, ['observer']); assert.equal(game.status, 'finished');
});
+33 -15
View File
@@ -48,9 +48,14 @@ function reachMentat(game: Game): Game {
g = applyAction(g, g.decision.player, { type: 'decision', done: true });
return g;
}
function finishMentat(game: Game): Game {
let g = game;
for (const p of g.players) g = applyAction(g, p.id, { type: 'ready' });
return g;
}
function offer(game: Game): NonNullable<Game['victoryContinuation']> {
const pending = game.victoryContinuation;
assert.ok(pending, 'The original Collection and Mentat transition offers a public agreement');
assert.ok(pending, 'The Mentat victory check offers a public agreement');
return pending;
}
function act(game: Game, player: string, mode: 'finish' | 'continue' | 'accept', extra?: number): Game {
@@ -72,10 +77,12 @@ function completeVotes(game: Game, extra: number): Game {
}
void test('Advanced public simultaneous victory waits for each qualifying participant and rejects other actions', () => {
const game = reachMentat(position());
const opened = reachMentat(position());
assert.equal(opened.phase, 8); assert.ok(!opened.victoryContinuation);
const game = finishMentat(opened);
assert.equal(game.phase, 8); assert.equal(game.status, 'playing'); assert.deepEqual(game.winner, []);
const pending = offer(game);
assert.equal(pending.resume, 'mentat');
assert.equal(pending.resume, 'advance');
assert.deepEqual(pending.sides, [['ix'], ['at', 'em']]);
assert.deepEqual(pending.voters, ['ix', 'at', 'em']);
assert.deepEqual(pending.accepted, []);
@@ -91,7 +98,7 @@ void test('Advanced public simultaneous victory waits for each qualifying partic
});
void test('declining the common proposal settles the original winners and correct private prediction', () => {
const game = reachMentat(position({ prediction: true }));
const game = finishMentat(reachMentat(position({ prediction: true })));
const vote = offer(game);
assert.deepEqual(game.winner, []);
const publicIx = viewGame(game, 'ix').victoryContinuation;
@@ -105,11 +112,11 @@ void test('declining the common proposal settles the original winners and correc
assert.ok(!next.victoryContinuation);
});
void test('unanimous continuation raises the ordinary solo and alliance targets without losing Mentat', () => {
const original = reachMentat(position());
void test('unanimous continuation raises targets and advances once after completed Mentat', () => {
const original = finishMentat(reachMentat(position()));
const beforeForces = original.players.map(player => ({ id: player.id, forces: player.forces, reserves: player.reserves }));
const next = completeVotes(original, 2);
assert.equal(next.status, 'playing'); assert.equal(next.phase, 8); assert.equal(next.turn, original.turn);
assert.equal(next.status, 'playing'); assert.equal(next.phase, 0); assert.equal(next.turn, original.turn + 1);
assert.deepEqual(next.winner, []); assert.ok(!next.victoryContinuation);
assert.equal(next.victoryExtraStrongholds, 2);
assert.deepEqual(next.players.map(player => ({ id: player.id, forces: player.forces, reserves: player.reserves })), beforeForces);
@@ -117,15 +124,11 @@ void test('unanimous continuation raises the ordinary solo and alliance targets
assert.equal(progress.find(row => row.player === 'ix')?.target, 5);
assert.equal(progress.find(row => row.player === 'at')?.target, 6);
assert.equal(progress.find(row => row.player === 'at')?.qualifies, false);
let completed = next;
for (const p of completed.players) completed = applyAction(completed, p.id, { type: 'ready' });
assert.equal(completed.turn, original.turn + 1);
assert.equal(completed.victoryExtraStrongholds, 2);
});
void test('zero-increase agreement preserves ordinary targets for the next turn', () => {
const next = completeVotes(reachMentat(position()), 0);
assert.equal(next.status, 'playing'); assert.equal(next.phase, 8);
const next = completeVotes(finishMentat(reachMentat(position())), 0);
assert.equal(next.status, 'playing'); assert.equal(next.phase, 0);
assert.equal(next.victoryExtraStrongholds ?? 0, 0);
const progress = viewGame(next, 'ix').victoryProgress;
assert.equal(progress.find(row => row.player === 'ix')?.target, 3);
@@ -138,13 +141,28 @@ void test('Basic multiple qualifiers can agree while one qualifying side still w
assert.ok(basic.victoryContinuation);
const settled = act(basic, basic.victoryContinuation.voters[0], 'finish');
assert.equal(settled.status, 'finished');
const single = reachMentat(position({ onlyAlliance: true }));
const single = finishMentat(reachMentat(position({ onlyAlliance: true })));
assert.equal(single.status, 'finished'); assert.ok(single.winner.length > 0);
assert.ok(!single.victoryContinuation);
});
void test('a saved legacy Advanced agreement resumes its original Mentat opportunity', () => {
// Stage the old opening-time receipt using the still-supported Basic path.
// This controlled saved frame is not a historical-room recovery proof.
const game = reachMentat(position({ advanced: false }));
assert.equal(offer(game).resume, 'mentat');
game.advanced = true;
assert.equal(game.mentatVictoryAfterActions, undefined);
const next = completeVotes(game, 1);
assert.equal(next.status, 'playing'); assert.equal(next.phase, 8);
assert.equal(next.turn, game.turn); assert.ok(!next.victoryContinuation);
const advanced = finishMentat(next);
assert.equal(advanced.turn, game.turn + 1);
assert.equal(advanced.phase, 0);
});
void test('all four minimal bot profiles produce a legal response for the public continuation', () => {
const game = reachMentat(position());
const game = finishMentat(reachMentat(position()));
for (const level of DIFFICULTIES) {
const view = viewGame(game, 'ix');
view.players.find(player => player.id === 'ix')!.bot = level;
+9 -1
View File
@@ -53,6 +53,11 @@ function enterMentat(source: Game): Game {
g = applyAction(g, g.decision.player, { type: 'decision', done: true });
assert.equal(g.decision?.kind, 'choamMentat');
g = applyAction(g, g.decision.player, { type: 'decision', done: true });
} else if (g.advanced) {
// Fresh Advanced Mentat evaluates victory after all ordinary opportunities.
assert.equal(g.status, 'playing');
assert.ok(!g.victoryContinuation);
for (const p of g.players) g = applyAction(g, p.id, { type: 'ready' });
}
return g;
}
@@ -110,7 +115,7 @@ void test('unanimous turn-ten agreement opens real turn eleven without replaying
const opening = enterMentat(original);
const before = opening.players.map(p => ({ id: p.id, forces: p.forces, reserves: p.reserves }));
const agreed = accept(opening, 1);
assert.equal(agreed.turn, 10); assert.equal(agreed.phase, 8);
assert.equal(agreed.turn, 11); assert.equal(agreed.phase, 0);
assert.equal(agreed.victoryFinalTurn, 11); assert.equal(agreed.victoryExtraStrongholds, 1);
assert.deepEqual(agreed.players.map(p => ({ id: p.id, forces: p.forces, reserves: p.reserves })), before);
const next = finishMentat(agreed);
@@ -151,6 +156,9 @@ void test('accepted turn eleven follows native phases through its next Mentat ch
}
assert.equal(game.turn, 11);
assert.equal(game.phase, 8, 'The original phase sequence reaches the next Mentat check');
assert.equal(game.status, 'playing');
assert.ok(!game.victoryContinuation);
for (const p of game.players) game = actAdvancedRevivalOrder(game, p.id, { type: 'ready' }, actions);
assert.ok(game.status === 'finished' || game.victoryContinuation,
'The extended final-turn check settles a winner or offers the next agreement');
assert.equal(game.victoryFinalTurn, 11);
+4
View File
@@ -62,6 +62,10 @@ function actual(g: Game) {
g = structuredClone(g);
g.phase = 7;
for (const p of g.players) g = applyAction(g, p.id, { type: 'ready' });
if (g.advanced && g.phase === 8 && g.status === 'playing' && !g.victoryContinuation) {
// Fresh Advanced games check the quote after every Mentat opportunity.
for (const p of g.players) g = applyAction(g, p.id, { type: 'ready' });
}
return g;
}
function compare(g: Game, winner: string[]) {