Files
Dune/tests/leader-skill-battle-fixture.ts
T
Quintine 06f46c6f79 Connect Leader Skill battle effects and private Rihani exchanges
Prototype Mentat/Bureaucrat score effects, automatic Sandmaster board spice, and Rihani inspection plus committed exchange. Preserve private views, physical custody, saved winner cleanup and captive/Suk sequencing. Remaining normal bands, missing skill families and combined modules remain gated.

Validation: 45 focused tests; typecheck, lint, 4465 offline tests, production build and 43 HTTP tests passed. Independent rules/privacy/persistence review completed. New isolated browser setup exercised inspection, draw, pending refresh, exchange and completed refresh. Two genuine four-profile Basic/Advanced samples completed without rejected candidates but did not trigger Rihani.

Preserved all 393 opening rooms and original seat/recovery rows (427 rooms after QA/HTTP). SQLite integrity and foreign keys passed. Server remained healthy after the reported outage; no restart or reset.

Verified source tree: c64ce9537269de94c0d785bc08b17346e826c5797352a636771e1dc6ce5e4859
2026-09-13 19:45:29 +10:00

102 lines
5.5 KiB
TypeScript

import assert from 'node:assert/strict';
import { mock } from 'node:test';
import { applyAction, createGame, initializeLeaderSkillsGameForAudit, joinGame, newPlayer, type Game } from '../game/engine';
import { LEADER_SKILL_CARDS, type LeaderSkillId } from '../game/leader-skill-cards';
import { validateLeaderSkills } from '../game/leader-skills';
function dealtSkillGame(native: 'emperor' | 'atreides' | 'harkonnen', advanced: boolean, skill: LeaderSkillId = 'suk-graduate'): Game {
let game = createGame('SUKTEST', newPlayer('a', 'Rescuer', native), advanced);
joinGame(game, newPlayer('d', 'Opponent', 'guild'));
for (const player of game.players) game = applyAction(game, player.id, { type: 'ready' });
const index = LEADER_SKILL_CARDS.findIndex((card) => card.id === skill);
let shuffleIndex = LEADER_SKILL_CARDS.length - 1;
mock.method(globalThis.crypto, 'getRandomValues', (array: Uint32Array) => {
array[0] = shuffleIndex-- === index ? 0 : 0xffffffff;
return array;
});
try { game = initializeLeaderSkillsGameForAudit(game); }
finally { mock.restoreAll(); }
assert.equal(game.leaderSkills!.offers.a.cards[0], skill);
return game;
}
export function advancedAtreidesSukOffer(): Game {
return dealtSkillGame('atreides', true);
}
/** Genuine module setup, then a conserved, explicitly staged battle position. */
export function leaderSkillBattle({ skill = 'suk-graduate' as LeaderSkillId, hide = true, advanced = false, elite = false, bluff = false, captured = false, capturedNativeSkill = undefined as LeaderSkillId | undefined, weaponKills = false, dial = 4, atreides = false, territory = 'arrakeen', sector = 10 } = {}): Game {
const native = captured ? 'harkonnen' : atreides ? 'atreides' : 'emperor';
let game = dealtSkillGame(native, advanced, skill);
for (const id of ['a', 'd']) {
const offer = game.leaderSkills!.offers[id];
const selected = id === 'a' && advanced && atreides && skill === 'suk-graduate' ? offer.cards[1] : offer.cards[0];
game = applyAction(game, id, { type: 'leaderSkill', event: offer.event,
skill: selected, leader: id === 'a' ? `${native}-0` : 'guild-0' });
}
if (advanced && atreides && skill === 'suk-graduate') {
// Stage an old saved assignment only after setup legally returned Suk to the deck.
const assignment = game.leaderSkills!.assignments.find((a) => a.owner === 'a')!;
const sukIndex = game.leaderSkills!.deck.indexOf('suk-graduate');
assert.ok(sukIndex >= 0);
game.leaderSkills!.deck[sukIndex] = assignment.skill;
assignment.skill = 'suk-graduate';
validateLeaderSkills(game.leaderSkills!, game.players);
}
for (let i = 0; game.status === 'setup' && i < 10; i++) {
const owner = game.players.find((p) => p.traitorChoices.length);
assert.ok(owner);
game = applyAction(game, owner.id, { type: 'traitor', leader: owner.traitorChoices[0] });
}
assert.equal(game.status, 'playing');
if (captured) {
const [own, other] = game.leaderSkills!.assignments;
[own.skill, other.skill] = [other.skill, own.skill];
game.players[1].leaders[0].capturedBy = 'a';
if (capturedNativeSkill && own.skill !== capturedNativeSkill) {
const index = game.leaderSkills!.deck.indexOf(capturedNativeSkill);
assert.ok(index >= 0);
[own.skill, game.leaderSkills!.deck[index]] = [game.leaderSkills!.deck[index], own.skill];
}
}
const battleKey = `${territory}:${sector}`;
for (const player of game.players) {
game.deck.push(...player.hand); player.hand = [];
player.forces = { [battleKey]: 5 }; player.reserves = 15; player.tanks = 0;
player.spice = 20; player.battleLosses = 0;
if (player.elites) player.elites = { forces: {}, reserves: 5, tanks: 0, revived: 0 };
}
if (elite) game.players[0].elites = { forces: { [battleKey]: 2 }, reserves: 3, tanks: 0, revived: 0 };
const cardIndex = game.deck.findIndex((card) => card.kind === 'worthless');
assert.ok(cardIndex >= 0);
const [played] = game.deck.splice(cardIndex, 1);
game.players[0].hand.push(played);
let enemyWeapon: string | undefined;
if (weaponKills) {
const index = game.deck.findIndex((card) => card.kind === 'projectile');
assert.ok(index >= 0);
const [weapon] = game.deck.splice(index, 1);
game.players[1].hand.push(weapon); enemyWeapon = weapon.id;
}
Object.assign(game, { phase: 6, storm: 18, order: ['a', 'd'], active: 'a', ready: [], decision: null, response: null, phaseOpening: null });
game = applyAction(game, 'a', { type: 'chooseBattle', territory, target: 'd' });
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 });
}
while (game.battle?.preparation)
game = applyAction(game, game.battle.preparation.owner, { type: 'declineBattlePower' });
if (game.decision?.kind === 'fullPlanOffer')
game = applyAction(game, game.decision.player, { type: 'decision', decline: true });
game = applyAction(game, 'a', { type: 'battlePlan', dial, support: advanced ? 3 : 0,
leader: captured ? 'guild-0' : hide && !bluff ? `${native}-0` : `${native}-1`, weapon: played.id });
game = applyAction(game, 'd', { type: 'battlePlan', dial: 0, leader: 'guild-1', weapon: enemyWeapon });
game = applyAction(game, 'a', { type: 'traitorCall', call: false });
return game;
}
export function resolveLeaderSkillBattle(game: Game): Game {
return applyAction(game, 'd', { type: 'traitorCall', call: false });
}