Files
Dune/tests/choam-skills-fixture.ts
Quintine d1bf33d0bc Connect Basic CHOAM skills and preserve market Ghola choices
Connect ordinary Leader Skill paths to Basic CHOAM with base opponents,
the real 35-card Treachery deck and all fourteen skills. Retain wider
development initializer behavior and existing mode/publication gates.

Pause a declared sale until its Ghola revival and private skill choice
finish. Bind the actual revived leader, offer event and completion to the
saved interruption. Preserve nested Truthtrance disposal and exact sale
responses; canceling a resumed sale leaves the free revival completed.
Keep force-only and retained-skill revivals free of replacement offers.

Add genuine setup, native card-sale, mandatory disposal, private custody,
corruption and concurrent authenticated recovery regressions plus a
two-through-six-player complete-game sample profile. Record the unresolved
Sandmaster/native-HMS eligibility question without inventing a bonus.

Validation: types, lint, 5101 offline tests and production build passed.
Five genuine 2-6 player CHOAM Skills games completed with zero rejected
actions. Independent review and 50 focused compatibility tests passed.
Browser Y2UJGMCS exercised real setup, Ghola draw, exact private refresh,
mobile assignment and resumed sale paying two spice once. Screenshot
capture timed out; full visual acceptance remains open.
All 1,583 prior rooms, 3,351 seats and auxiliary records remain intact;
one QA room and human seat were added. No restart, reset or automation.

Full AI implementation and tuning remain deferred until all non-AI
features are complete, then target roughly 75% for each adjacent tier.
2026-09-21 15:10:29 +10:00

185 lines
6.0 KiB
TypeScript

import assert from 'node:assert/strict';
import { mock } from 'node:test';
import { treacheryDeck } from '../game/cards';
import {
applyAction,
createGame,
initializeLeaderSkillsGameForAudit,
joinGame,
newPlayer,
type Action,
type Game,
type Player,
} from '../game/engine';
import {
LEADER_SKILL_CARDS,
type LeaderSkillId,
} from '../game/leader-skill-cards';
import { traitorDeck } from '../game/traitors';
export const CHOAM_SKILLS_SEATS = {
choam: 'c',
atreides: 'a',
emperor: 'e',
} as const;
export type ChoamSkillsFaction = keyof typeof CHOAM_SKILLS_SEATS;
export type ChoamSkillsFixtureOptions = {
requestedSkill?: LeaderSkillId;
skillOwner?: ChoamSkillsFaction;
seats?: Readonly<Record<ChoamSkillsFaction, string>>;
code?: string;
};
const names: Record<ChoamSkillsFaction, string> = {
choam: 'CHOAM',
atreides: 'Atreides',
emperor: 'Emperor',
};
export const reloadChoamSkillsGame = (game: Game): Game =>
JSON.parse(JSON.stringify(game)) as Game;
export function choamSkillsPlayer(game: Game, id: string): Player {
const player = game.players.find((candidate) => candidate.id === id);
assert.ok(player);
return player;
}
export function rejectChoamSkillsAction(
game: Game,
id: string,
action: Action,
pattern?: RegExp,
): void {
const before = structuredClone(game);
if (pattern) assert.throws(() => applyAction(game, id, action), pattern);
else assert.throws(() => applyAction(game, id, action));
assert.deepEqual(game, before);
}
/** Count only physical inventories; projections and history are not custody. */
export function assertChoamSkillsCustody(game: Game): void {
const skills = game.leaderSkills!;
const physicalSkills = [
...skills.deck,
...Object.values(skills.offers).flatMap((offer) => offer.cards),
...skills.assignments.map((assignment) => assignment.skill),
];
const canonicalSkills = LEADER_SKILL_CARDS.map((card) => card.id);
assert.equal(physicalSkills.length, 14);
assert.equal(new Set(physicalSkills).size, 14);
assert.deepEqual([...physicalSkills].sort(), [...canonicalSkills].sort());
const physicalTreachery = [
...game.deck,
...game.discard,
...game.players.flatMap((player) => player.hand),
...(game.auction?.cards.slice(
game.auction.index + (game.currentAuctionSale ? 1 : 0),
) ?? []),
].map((card) => card.id);
const canonicalTreachery = treacheryDeck(['choam']).map((card) => card.id);
assert.equal(physicalTreachery.length, 35);
assert.equal(new Set(physicalTreachery).size, 35);
assert.deepEqual(physicalTreachery.sort(), canonicalTreachery.sort());
for (const player of game.players) {
const forces =
player.reserves +
player.tanks +
Object.values(player.forces).reduce((sum, count) => sum + count, 0);
assert.equal(
forces,
20,
`${player.name} must retain all twenty physical forces`,
);
}
if (game.status === 'playing' || game.status === 'finished') {
const physicalTraitors = [
...game.traitorReserve!,
...game.players.flatMap((player) => player.traitors),
];
const canonicalTraitors = traitorDeck(game.players, game.advanced);
assert.equal(physicalTraitors.length, canonicalTraitors.length);
assert.equal(new Set(physicalTraitors).size, physicalTraitors.length);
assert.deepEqual(physicalTraitors.sort(), canonicalTraitors.sort());
}
}
/** Real ready lobby and setup deal; only the production skill shuffle is controlled. */
export function initializedChoamSkillsOffers(
options: ChoamSkillsFixtureOptions = {},
): Game {
const requestedSkill = options.requestedSkill ?? 'planetologist';
const skillOwner = options.skillOwner ?? 'choam';
const seats = options.seats ?? CHOAM_SKILLS_SEATS;
const factions = (
[skillOwner, 'choam', 'atreides', 'emperor'] as ChoamSkillsFaction[]
).filter((faction, index, all) => all.indexOf(faction) === index);
const first = factions[0];
let game = createGame(
options.code ?? 'CHOSKILL',
newPlayer(seats[first], names[first], first),
false,
['choam'],
);
for (const faction of factions.slice(1))
joinGame(game, newPlayer(seats[faction], names[faction], faction));
for (const player of game.players)
game = applyAction(game, player.id, { type: 'ready' });
const targetIndex = LEADER_SKILL_CARDS.findIndex(
(card) => card.id === requestedSkill,
);
assert.ok(targetIndex >= 0);
let shuffleIndex = LEADER_SKILL_CARDS.length - 1;
mock.method(globalThis.crypto, 'getRandomValues', (array: Uint32Array) => {
array[0] = shuffleIndex-- === targetIndex ? 0 : 0xffffffff;
return array;
});
try {
game = initializeLeaderSkillsGameForAudit(game);
} finally {
mock.restoreAll();
}
assert.equal(game.status, 'setup');
assert.equal(game.setupStage, 'leaderSkills');
assert.equal(
game.leaderSkills!.offers[seats[skillOwner]].cards[0],
requestedSkill,
);
assertChoamSkillsCustody(game);
return game;
}
/** Complete genuine skill assignment and ordinary Traitor selection. */
export function completedChoamSkillsGame(
options: ChoamSkillsFixtureOptions = {},
): Game {
const requestedSkill = options.requestedSkill ?? 'planetologist';
const seats = options.seats ?? CHOAM_SKILLS_SEATS;
const requestedOwner = seats[options.skillOwner ?? 'choam'];
let game = initializedChoamSkillsOffers(options);
for (const player of game.players) {
const offer = game.leaderSkills!.offers[player.id];
game = applyAction(game, player.id, {
type: 'leaderSkill',
event: offer.event,
skill: player.id === requestedOwner ? requestedSkill : offer.cards[0],
leader: player.leaders[0].id,
});
}
assert.equal(game.setupStage, 'traitors');
for (const player of game.players) {
const choice = choamSkillsPlayer(game, player.id).traitorChoices[0];
assert.ok(choice);
game = applyAction(game, player.id, { type: 'traitor', leader: choice });
}
assert.equal(game.status, 'playing');
assert.equal(game.setupStage, undefined);
assertChoamSkillsCustody(game);
return game;
}