116 lines
6.4 KiB
TypeScript
116 lines
6.4 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { applyAction, createGame, initializeEcazOccupyGameForAudit, joinGame,
|
|
newPlayer, viewGame, type Game } from '../game/engine';
|
|
import { defaultHarassWithdrawAllocation } from '../game/harass-withdraw';
|
|
import { stageEcazOccupyCard } from './fixture-ecaz-occupy';
|
|
import { NO_FIELD_HARASS, NO_FIELD_REINFORCEMENTS } from './fixture-no-field-reinforcements';
|
|
import { ecazOccupyRicheseFixture, finishOccupyRichese, nextOccupyRichese,
|
|
occupyRicheseSeat, openOccupyRichese } from './fixture-ecaz-occupy-richese';
|
|
|
|
/** Select the independent physical cards before the original Basic deal. */
|
|
function basicSetup(): Game {
|
|
const roster = ['ecaz', 'richese', 'guild', 'choam'] as const;
|
|
let game = createGame('BASICMIXEDCARDS', newPlayer(roster[0], roster[0], roster[0]),
|
|
false, ['ecaz', 'choam']);
|
|
for (const faction of roster.slice(1)) joinGame(game, newPlayer(faction, faction, faction));
|
|
for (let i = roster.length - 1; i >= 0; i--) {
|
|
if (viewGame(game, roster[i]).playerPositions[roster[i]] !== i + 1)
|
|
game = applyAction(game, roster[i], { type: 'seatPosition', position: i + 1 });
|
|
}
|
|
for (const player of game.players) game = applyAction(game, player.id, { type: 'ready' });
|
|
game.ecazTreachery = true;
|
|
const descriptor = Object.getOwnPropertyDescriptor(crypto, 'getRandomValues');
|
|
crypto.getRandomValues = <T extends ArrayBufferView | null>(array: T): T => {
|
|
assert.ok(array);
|
|
new Uint8Array(array.buffer, array.byteOffset, array.byteLength).fill(255);
|
|
return array;
|
|
};
|
|
try { return initializeEcazOccupyGameForAudit(game); }
|
|
finally {
|
|
if (descriptor) Object.defineProperty(crypto, 'getRandomValues', descriptor);
|
|
else Reflect.deleteProperty(crypto, 'getRandomValues');
|
|
}
|
|
}
|
|
|
|
/** Finish real post-reveal allocation and traitor opportunities. */
|
|
function finishBattle(state: Game, owner: string, opponent: string): Game {
|
|
let game = state;
|
|
for (let attempt = 0; attempt < 80; attempt++) {
|
|
if (game.response || game.phaseOpening) {
|
|
game = nextOccupyRichese(game);
|
|
continue;
|
|
}
|
|
if (game.decision?.kind === 'harassWithdraw') {
|
|
const decision = game.decision;
|
|
const allocation = viewGame(game, decision.player).battle?.harassAllocation;
|
|
assert.ok(allocation);
|
|
game = applyAction(game, decision.player, { type: 'decision', event: decision.event,
|
|
returns: defaultHarassWithdrawAllocation(allocation.context, allocation.dial, allocation.support) });
|
|
continue;
|
|
}
|
|
const battle = viewGame(game, owner).battle;
|
|
const voter = !game.decision && battle?.traitorVoters.find(id => !battle.traitorSubmitted.includes(id));
|
|
if (!voter) return finishOccupyRichese(game);
|
|
assert.ok(voter === owner || voter === opponent || game.players.some(player => player.id === voter));
|
|
game = applyAction(game, voter, { type: 'traitorCall', call: false });
|
|
}
|
|
throw Error('Basic mixed Occupy card battle did not finish its native decisions.');
|
|
}
|
|
|
|
for (const lead of ['ecaz', 'ally'] as const) {
|
|
void test(`Basic mixed Richese marker with ${lead} leading resolves both independent battle cards`, () => {
|
|
const source = ecazOccupyRicheseFixture({ initial: basicSetup(), advanced: false,
|
|
allyFaction: 'richese', opponentFaction: 'guild', marker: 3,
|
|
mixedRicheseForces: 2, richeseReserves: 8 });
|
|
assert.equal(source.initial.advanced, false);
|
|
assert.equal(source.initial.ecazTreachery, true);
|
|
const owner = lead === 'ecaz' ? source.ecaz : source.richese;
|
|
// Conserved staging: return dealt faces to their actual deck, then give
|
|
// the lead the two original physical cards before choosing the battle lead.
|
|
for (const seat of source.game.players) {
|
|
source.game.deck.push(...seat.hand);
|
|
seat.hand = [];
|
|
}
|
|
stageEcazOccupyCard(source.game, owner, card => card.id === NO_FIELD_HARASS);
|
|
stageEcazOccupyCard(source.game, owner, card => card.id === NO_FIELD_REINFORCEMENTS);
|
|
let game = openOccupyRichese(source, lead);
|
|
const original = structuredClone(game);
|
|
const offer = viewGame(game, owner).battle!;
|
|
assert.equal(offer.harassWithdraw?.blocked, null);
|
|
assert.equal(offer.reinforcements?.blocked, null);
|
|
const profile = offer.ecazOccupy!.profile;
|
|
assert.ok(profile);
|
|
assert.equal(profile.fixedEcazDial, 3);
|
|
const leader = occupyRicheseSeat(game, owner).leaders.filter(face => !face.dead && !face.usedAt)
|
|
.sort((a, b) => b.strength - a.strength || a.id.localeCompare(b.id))[0];
|
|
const enemy = occupyRicheseSeat(game, source.opponent).leaders.filter(face => !face.dead && !face.usedAt)
|
|
.sort((a, b) => a.strength - b.strength || a.id.localeCompare(b.id))[0];
|
|
assert.ok(leader && enemy);
|
|
const marker = structuredClone(occupyRicheseSeat(game, source.richese).noField);
|
|
game = applyAction(game, owner, { type: 'battlePlan', dial: 6, support: 0,
|
|
leader: leader.id, weapon: NO_FIELD_REINFORCEMENTS, defense: NO_FIELD_HARASS });
|
|
assert.deepEqual(occupyRicheseSeat(game, source.richese).noField, marker);
|
|
assert.equal(occupyRicheseSeat(game, source.richese).reserves, 8);
|
|
game = applyAction(game, source.opponent, { type: 'battlePlan', dial: 0, support: 0, leader: enemy.id });
|
|
assert.equal(game.battle?.revealed, true);
|
|
assert.equal(occupyRicheseSeat(game, source.richese).noField!.deployed, null);
|
|
assert.equal(occupyRicheseSeat(game, source.richese).forces[source.location], 5);
|
|
assert.equal(occupyRicheseSeat(game, source.richese).reserves, 5);
|
|
game = finishBattle(game, owner, source.opponent);
|
|
assert.equal(game.lastBattleContext?.result, 'normal');
|
|
assert.equal(game.lastBattleContext?.winner, owner);
|
|
const ecaz = occupyRicheseSeat(game, source.ecaz);
|
|
const richese = occupyRicheseSeat(game, source.richese);
|
|
const returned = 2;
|
|
assert.equal(occupyRicheseSeat(game, owner).reserves,
|
|
occupyRicheseSeat(original, owner).reserves - (lead === 'ally' ? 3 : 0) - 3 + returned);
|
|
assert.equal(ecaz.tanks - occupyRicheseSeat(original, source.ecaz).tanks, 3 + (lead === 'ecaz' ? 3 : 0));
|
|
assert.equal(richese.tanks - occupyRicheseSeat(original, source.richese).tanks, 3 + (lead === 'ally' ? 3 : 0));
|
|
assert.equal(ecaz.forces[source.location] ?? 0, lead === 'ecaz' ? 0 : 2);
|
|
assert.equal(richese.forces[source.location] ?? 0, lead === 'ally' ? 0 : 2);
|
|
for (const card of [NO_FIELD_REINFORCEMENTS, NO_FIELD_HARASS])
|
|
assert.equal(game.discard.filter(face => face.id === card).length, 1);
|
|
});
|
|
}
|