Restore fresh Ecaz coalition and Advanced Moritani initialization; retain printed native continuations and selected-family card eligibility. Validate Mentat fallback hands against explicitly selected independent cards. Frozen25-file274/274, types/lint/build and genuine runtime/mobile entry proofs pass. No save retrofit or deployment.
146 lines
4.6 KiB
TypeScript
146 lines
4.6 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { mock } from 'node:test';
|
|
import {
|
|
applyAction,
|
|
createGame,
|
|
joinGame,
|
|
newPlayer,
|
|
viewGame,
|
|
type Game,
|
|
} from '../game/engine';
|
|
import { LEADER_SKILL_CARDS } from '../game/leader-skill-cards';
|
|
import { botActions } from '../game/bots';
|
|
|
|
export const mentatReload = (game: Game): Game =>
|
|
JSON.parse(JSON.stringify(game)) as Game;
|
|
export const mentatPlayer = (game: Game, id: string) =>
|
|
game.players.find((player) => player.id === id)!;
|
|
export function takeMentatCard(game: Game, player: string, name: string) {
|
|
const pool =
|
|
name === 'Stone Burner' || name === 'Mirror Weapon'
|
|
? game.richeseCache!
|
|
: game.deck;
|
|
const index = pool.findIndex((card) => card.name === name);
|
|
assert.ok(index >= 0, `Missing fixture card ${name}`);
|
|
const [card] = pool.splice(index, 1);
|
|
mentatPlayer(game, player).hand.push(card);
|
|
return card;
|
|
}
|
|
/** Genuine public skill assignment and setup, followed by a conserved battle position. */
|
|
export function mentatQuestionGame({
|
|
advanced = false,
|
|
choam = false,
|
|
independent = false,
|
|
cards = ['Crysknife', 'Snooper'],
|
|
begin = true,
|
|
captured = false,
|
|
}: {
|
|
advanced?: boolean;
|
|
choam?: boolean;
|
|
independent?: boolean;
|
|
cards?: string[];
|
|
begin?: boolean;
|
|
captured?: boolean;
|
|
} = {}): Game {
|
|
const native = choam ? 'richese' : 'emperor';
|
|
let game = createGame(
|
|
'MENTATQA',
|
|
newPlayer('a', 'Mentat owner', native),
|
|
advanced,
|
|
choam ? ['choam'] : [],
|
|
);
|
|
joinGame(
|
|
game,
|
|
newPlayer('d', 'Questioned opponent', captured ? 'harkonnen' : 'atreides'),
|
|
);
|
|
joinGame(game, newPlayer('o', 'Observer', 'guild'));
|
|
game = applyAction(game, game.host, { type: 'moduleOption', module: 'leaderSkills', enabled: true });
|
|
if (independent)
|
|
game = applyAction(game, game.host, { type: 'moduleOption', module: 'ecazTreachery', enabled: true });
|
|
for (const player of game.players)
|
|
game = applyAction(game, player.id, { type: 'ready' });
|
|
const index = LEADER_SKILL_CARDS.findIndex((card) => card.id === 'mentat');
|
|
let cursor = LEADER_SKILL_CARDS.length - 1;
|
|
mock.method(globalThis.crypto, 'getRandomValues', (array: Uint32Array) => {
|
|
array[0] = cursor-- === index ? 0 : 0xffffffff;
|
|
return array;
|
|
});
|
|
try {
|
|
game = applyAction(game, game.host, advanced ? { type: 'start', advancedPreview: true } : { type: 'start' });
|
|
} finally {
|
|
mock.restoreAll();
|
|
}
|
|
assert.ok(game.leaderSkills!.offers.a.cards.includes('mentat'));
|
|
for (let step = 0; game.status === 'setup' && step < 80; step++) {
|
|
let progress = false;
|
|
for (const player of game.players) {
|
|
const offer = game.leaderSkills!.offers[player.id];
|
|
const actions =
|
|
game.setupStage === 'leaderSkills' && offer
|
|
? [
|
|
{
|
|
type: 'leaderSkill',
|
|
event: offer.event,
|
|
skill: player.id === 'a' ? 'mentat' : offer.cards[0],
|
|
leader: `${player.faction}-0`,
|
|
},
|
|
]
|
|
: game.setupStage === 'traitors' && player.traitorChoices.length
|
|
? [{ type: 'traitor', leader: player.traitorChoices[0] }]
|
|
: botActions(viewGame(game, player.id));
|
|
for (const action of actions)
|
|
try {
|
|
game = applyAction(game, player.id, action);
|
|
progress = true;
|
|
break;
|
|
} catch {}
|
|
if (progress) break;
|
|
}
|
|
assert.ok(progress, `Setup stalled at ${game.setupStage}`);
|
|
}
|
|
assert.equal(game.status, 'playing');
|
|
for (const player of game.players) {
|
|
game.deck.push(...player.hand);
|
|
Object.assign(player, {
|
|
hand: [],
|
|
forces: player.id === 'o' ? {} : { 'arrakeen:10': 5 },
|
|
reserves: player.id === 'o' ? 20 : 15,
|
|
tanks: 0,
|
|
spice: 20,
|
|
});
|
|
}
|
|
takeMentatCard(game, 'a', 'Baliset');
|
|
takeMentatCard(game, 'o', 'Karama');
|
|
for (const name of cards) takeMentatCard(game, 'd', name);
|
|
if (captured) mentatPlayer(game, 'a').leaders[0].capturedBy = 'd';
|
|
Object.assign(game, {
|
|
phase: 6,
|
|
turn: 2,
|
|
storm: 18,
|
|
active: 'a',
|
|
order: ['a', 'd', 'o'],
|
|
ready: [],
|
|
decision: null,
|
|
response: null,
|
|
phaseOpening: null,
|
|
});
|
|
return begin
|
|
? applyAction(game, 'a', {
|
|
type: 'chooseBattle',
|
|
territory: 'arrakeen',
|
|
target: 'd',
|
|
})
|
|
: game;
|
|
}
|
|
export function finishMentatPosture(game: Game, hide = true): Game {
|
|
while (game.decision?.kind === 'leaderSkillVisibility') {
|
|
const decision = game.decision;
|
|
game = applyAction(game, decision.player, {
|
|
type: 'leaderSkillVisibility',
|
|
event: decision.event,
|
|
hide: decision.player === 'a' ? hide : false,
|
|
});
|
|
}
|
|
return game;
|
|
}
|