Prototype one free accompanying force into an empty territory with explicit opt-out, shared human/AI pricing, physical elite custody and Guild recipient accounting. Bind pending quotes to their unique saved Guild decision; preserve legacy unstamped full-price declarations. Smuggler battle collection and combined modules remain unfinished and gated. Repair a randomly dealt-out Karama fixture by relocating its actual physical card without changing inventory. Validation: 70 focused tests; typecheck, lint, 4477 offline tests, production build and 43 HTTP tests passed. Independent rules/persistence review completed, including interruption and cancellation. Browser checked the price toggle, three arrivals for two spice, and refresh with unchanged private cards. Two genuine four-profile Basic/Advanced samples completed without rejected candidates but did not use Smuggler. Preserved all 427 opening rooms and original seat/recovery rows (461 rooms after QA/HTTP). SQLite integrity and foreign keys passed. Server remained healthy after the reported power outage; no restart or reset. Verified source tree: bf6e02bf6873ca6d6b01c83481489f55b2c7db0184a7056364343440dae88803
120 lines
3.6 KiB
TypeScript
120 lines
3.6 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { mock } from 'node:test';
|
|
import {
|
|
applyAction,
|
|
createGame,
|
|
newPlayer,
|
|
joinGame,
|
|
initializeLeaderSkillsGameForAudit,
|
|
viewGame,
|
|
type Game,
|
|
} from '../game/engine';
|
|
import { LEADER_SKILL_CARDS } from '../game/leader-skill-cards';
|
|
import type { FactionId } from '../game/catalog';
|
|
import { botActions } from '../game/bots';
|
|
|
|
/** Genuine module setup followed by a conserved, explicitly staged shipment turn. */
|
|
export function smugglerShipmentGame(
|
|
faction: FactionId = 'emperor',
|
|
advanced = false,
|
|
): Game {
|
|
let g = createGame('SMUGSHIP', newPlayer('p', 'Smuggler', faction), advanced);
|
|
joinGame(
|
|
g,
|
|
newPlayer('h', 'Opponent', faction === 'guild' ? 'emperor' : 'guild'),
|
|
);
|
|
for (const p of g.players) g = applyAction(g, p.id, { type: 'ready' });
|
|
const index = LEADER_SKILL_CARDS.findIndex((c) => c.id === 'smuggler');
|
|
let cursor = LEADER_SKILL_CARDS.length - 1;
|
|
mock.method(globalThis.crypto, 'getRandomValues', (array: Uint32Array) => {
|
|
array[0] = cursor-- === index ? 0 : 0xffffffff;
|
|
return array;
|
|
});
|
|
try {
|
|
g = initializeLeaderSkillsGameForAudit(g);
|
|
} finally {
|
|
mock.restoreAll();
|
|
}
|
|
for (let i = 0; g.status === 'setup' && i < 60; i++) {
|
|
let accepted = false;
|
|
for (const p of g.players) {
|
|
const view = viewGame(g, p.id);
|
|
const skills = view.leaderSkills!;
|
|
const options =
|
|
g.setupStage === 'leaderSkills' && skills.offer
|
|
? [
|
|
{
|
|
type: 'leaderSkill',
|
|
event: skills.offer.event,
|
|
skill: p.id === 'p' ? 'smuggler' : skills.offer.cards[0],
|
|
leader: skills.eligibleLeaders[0].id,
|
|
},
|
|
]
|
|
: g.setupStage === 'traitors' && p.traitorChoices.length
|
|
? [{ type: 'traitor', leader: p.traitorChoices[0] }]
|
|
: g.setupStage === 'prediction' && p.faction === 'beneGesserit'
|
|
? [{ type: 'predict', faction: g.players[1].faction, turn: 3 }]
|
|
: g.setupStage === 'forces' &&
|
|
p.faction === 'fremen' &&
|
|
p.reserves === 20
|
|
? [
|
|
{
|
|
type: 'fremenSetup',
|
|
placements: {
|
|
sietch_tabr: 10,
|
|
false_wall_south: 0,
|
|
false_wall_west: 0,
|
|
},
|
|
},
|
|
]
|
|
: g.setupStage === 'forces' &&
|
|
p.faction === 'beneGesserit' &&
|
|
!p.advisorSetup
|
|
? [
|
|
{
|
|
type: 'advisorSetup',
|
|
territory: 'arrakeen',
|
|
sector: 10,
|
|
},
|
|
]
|
|
: botActions(view);
|
|
for (const a of options) {
|
|
try {
|
|
g = applyAction(g, p.id, a);
|
|
accepted = true;
|
|
break;
|
|
} catch {
|
|
/* Other seats can be waiting. */
|
|
}
|
|
}
|
|
if (accepted) break;
|
|
}
|
|
assert.ok(accepted, `setup ${g.setupStage}`);
|
|
}
|
|
assert.equal(g.status, 'playing');
|
|
Object.assign(g, {
|
|
phase: 5,
|
|
turn: 2,
|
|
storm: 18,
|
|
active: 'p',
|
|
order: ['p', 'h'],
|
|
movementRemaining: ['p', 'h'],
|
|
ready: [],
|
|
response: null,
|
|
decision: null,
|
|
phaseOpening: null,
|
|
});
|
|
for (const p of g.players) {
|
|
g.deck.push(...p.hand);
|
|
p.hand = [];
|
|
p.forces = {};
|
|
p.reserves = 20;
|
|
p.tanks = 0;
|
|
p.shipped = false;
|
|
p.moved = 0;
|
|
p.advisors = {};
|
|
if (p.elites) p.elites = { ...p.elites, forces: {}, reserves: 5, tanks: 0 };
|
|
}
|
|
return g;
|
|
}
|