143 lines
8.2 KiB
TypeScript
143 lines
8.2 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
|
|
import { TERRITORIES, location as locationKey } from '../game/board';
|
|
import { advanceToNextStorm } from './fixture-advanced-source';
|
|
import { basicEcazOccupyFixture, basicEcazOccupyBattleAction } from './fixture-basic-ecaz-occupy';
|
|
import { nextBasicEcazNativeStep } from './fixture-basic-ecaz-native';
|
|
import { settleEcazOccupyArrival, stageEcazOccupyCard } from './fixture-ecaz-occupy';
|
|
import { futureBattleLeaderRecords as records } from './fixture-truth-future-battle-leader';
|
|
|
|
const clean = (game: Game) => !game.response && !game.phaseOpening && !game.decision && !game.pendingTreacheryDiscard;
|
|
function next(game: Game): Game {
|
|
const step = nextBasicEcazNativeStep(game);
|
|
assert.ok(step, `The original continuation needs an action in phase ${game.phase}.`);
|
|
return applyAction(game, step.actor, step.action);
|
|
}
|
|
function advance(game: Game, until: (game: Game) => boolean): Game {
|
|
for (let i = 0; i < 1600 && !until(game); i++) game = next(game);
|
|
assert.ok(until(game), 'Native continuation did not reach the requested boundary.');
|
|
return game;
|
|
}
|
|
function arrangeBlow(game: Game, worm: boolean, position: number) {
|
|
const index = game.spiceDeck.findIndex((card, i) => i >= position && (worm
|
|
? 'worm' in card && !card.greatMaker && !card.suppressed
|
|
: 'territory' in card && card.territory !== 'the_great_flat'));
|
|
assert.ok(index >= position);
|
|
game.spiceDeck.splice(position, 0, game.spiceDeck.splice(index, 1)[0]);
|
|
}
|
|
|
|
/** The fixture starts with a genuine Basic deal. As in the original Occupy
|
|
* fixture, conserved board counters are returned, unopened Spice Cards are
|
|
* reordered, and a rival army, the shipping budgets, two Truthtrance cards and
|
|
* Karama are staged. Alliance, answers, allied shipments, battle and plan
|
|
* choices are real native actions. */
|
|
function source() {
|
|
const original = basicEcazOccupyFixture({ chooseBattle: false });
|
|
let game = structuredClone(original.afterSetup);
|
|
for (const player of game.players) {
|
|
player.reserves += Object.values(player.forces).reduce((a, b) => a + b, 0);
|
|
player.forces = {};
|
|
if (player.elites) {
|
|
player.elites.reserves += Object.values(player.elites.forces).reduce((a, b) => a + b, 0);
|
|
player.elites.forces = {};
|
|
}
|
|
}
|
|
// This fixture deliberately supplies a test-bank budget for the actual
|
|
// later paid shipments, as the selected battle needs four forces per ally.
|
|
for (const id of [original.ecaz, original.ally])
|
|
game.players.find(p => p.id === id)!.spice = 30;
|
|
// Stage before either actual auction can move an unopened face to discard.
|
|
const first = stageEcazOccupyCard(game, original.opponent, card => card.effect === 'truthtrance');
|
|
const second = stageEcazOccupyCard(game, original.opponent, card => card.effect === 'truthtrance' && card.id !== first);
|
|
stageEcazOccupyCard(game, original.opponent, card => card.effect === 'karama');
|
|
arrangeBlow(game, false, 0);
|
|
arrangeBlow(game, false, 1);
|
|
game = advance(game, g => g.phase === 5 && clean(g));
|
|
while (game.phase === 5) game = clean(game) ? applyAction(game, game.active!, { type: 'endMovement' }) : next(game);
|
|
game = advanceToNextStorm(game);
|
|
arrangeBlow(game, true, 0);
|
|
arrangeBlow(game, false, 1);
|
|
arrangeBlow(game, false, 2);
|
|
game = advance(game, g => g.phase === 1 && !!g.nexus && !g.spiceWindow && !g.spiceResolution && clean(g));
|
|
game = applyAction(game, original.ecaz, { type: 'alliance', target: original.ally });
|
|
game = applyAction(game, original.ally, { type: 'alliance', target: original.ecaz });
|
|
game = advance(game, g => g.phase === 5 && clean(g));
|
|
const sector = TERRITORIES.find(t => t.id === original.territory)!.sectors.find(s => s !== game.storm)!;
|
|
const location = locationKey(original.territory, sector);
|
|
const rival = game.players.find(p => p.id === original.opponent)!;
|
|
assert.ok(rival.reserves >= 8);
|
|
rival.reserves -= 8;
|
|
rival.forces[location] = 8;
|
|
const named: Record<string, string> = {};
|
|
for (const [target, card] of [[original.ecaz, first], [original.ally, second]] as const) {
|
|
const player = game.players.find(p => p.id === target)!;
|
|
const leader = player.leaders.find(l => !l.dead && !l.usedAt)!;
|
|
assert.ok(leader);
|
|
named[target] = leader.id;
|
|
assert.ok(viewGame(game, original.opponent).truthFutureBattleLeaderTargets.includes(target));
|
|
game = applyAction(game, original.opponent, { type: 'card', card });
|
|
while (game.truthtrance?.stage === 'priority') {
|
|
const actor = game.players.find(p => !game.truthtrance!.passed.includes(p.id));
|
|
assert.ok(actor);
|
|
game = applyAction(game, actor.id, { type: 'truthPass' });
|
|
}
|
|
assert.equal(game.truthtrance?.stage, 'ask');
|
|
game = applyAction(game, original.opponent, { type: 'truthAsk', question: {
|
|
kind: 'futureBattleLeader', target, turn: game.turn, leader: leader.id,
|
|
} } as Action);
|
|
game = applyAction(game, target, { type: 'truthAnswer', answer: 'yes' });
|
|
assert.equal(records(game).find(row => row.player === target)?.status, 'waiting');
|
|
}
|
|
while (game.phase === 5) {
|
|
if (!clean(game)) { game = next(game); continue; }
|
|
const actor = game.active!;
|
|
if (actor === original.ecaz || actor === original.ally) {
|
|
game = settleEcazOccupyArrival(applyAction(game, actor, { type: 'ship',
|
|
territory: original.territory, sector, amount: 4 }));
|
|
}
|
|
game = applyAction(game, actor, { type: 'endMovement' });
|
|
}
|
|
game = advance(game, g => g.phase === 6 && clean(g) && !g.battle && !!g.active);
|
|
return { ...original, game, named, location };
|
|
}
|
|
|
|
for (const lead of ['ecaz', 'ally'] as const) for (const canceled of [false, true])
|
|
void test(`Basic Occupy ${lead} lead ${canceled ? 'canceled' : 'allowed'} binds only its actual plan owner`, () => {
|
|
const f = source();
|
|
let game = f.game;
|
|
const battle = basicEcazOccupyBattleAction(f);
|
|
game = applyAction(game, battle.actor, battle.action);
|
|
assert.equal(game.decision?.kind, 'ecazBattleLead');
|
|
assert.ok(records(game).every(row => row.status === 'waiting' && !row.battle));
|
|
const selected = lead === 'ecaz' ? f.ecaz : f.ally;
|
|
const nonleader = selected === f.ecaz ? f.ally : f.ecaz;
|
|
game = applyAction(game, f.ecaz, { type: 'decision', event: game.decision!.event, lead: selected });
|
|
assert.equal(game.response?.kind, 'ecazOccupy');
|
|
assert.ok(records(game).every(row => row.status === 'waiting' && !row.battle));
|
|
if (canceled) {
|
|
const card = game.players.find(p => p.id === f.opponent)!.hand.find(c => c.effect === 'karama');
|
|
assert.ok(card);
|
|
game = applyAction(game, f.opponent, { type: 'card', mode: 'cancel', card: card.id });
|
|
}
|
|
game = advance(game, g => !!g.battle && clean(g) && !g.battle.preparation &&
|
|
(!g.battle.preLeader || g.battle.preLeader.closed));
|
|
assert.equal(game.battle!.ecazOccupy!.canceled, canceled);
|
|
assert.equal(records(game).find(row => row.player === selected)?.status, 'bound');
|
|
assert.equal(records(game).find(row => row.player === selected)?.battle, game.battle!.event);
|
|
assert.equal(records(game).find(row => row.player === nonleader)?.status, 'waiting');
|
|
const profile = viewGame(game, selected).battle!.ecazOccupy!.profile!;
|
|
const wrong = game.players.find(p => p.id === selected)!.leaders.find(l => l.id !== f.named[selected] && !l.dead && !l.usedAt)!;
|
|
const before = structuredClone(game);
|
|
assert.throws(() => applyAction(game, selected, { type: 'battlePlan', dial: profile.fixedEcazDial,
|
|
support: 0, leader: wrong.id }));
|
|
assert.deepEqual(game, before);
|
|
game = applyAction(game, selected, { type: 'battlePlan', dial: profile.fixedEcazDial,
|
|
support: 0, leader: f.named[selected] });
|
|
const opponentLeader = game.players.find(p => p.id === f.opponent)!.leaders.find(l => !l.dead && !l.usedAt)!;
|
|
game = applyAction(game, f.opponent, { type: 'battlePlan', dial: 0, support: 0, leader: opponentLeader.id });
|
|
assert.equal(records(game).find(row => row.player === selected)?.status, 'settled');
|
|
game = advance(game, g => !g.battle && (g.phase > 6 || g.turn > f.game.turn));
|
|
assert.equal(records(game).find(row => row.player === nonleader)?.status, 'expired');
|
|
});
|