325 lines
21 KiB
TypeScript
325 lines
21 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { applyAction, createGame, initializeLeaderSkillsGameForAudit, joinGame, newPlayer, viewGame, type Action, type Game } from '../game/engine';
|
|
import { botActions } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import { gameDistance, MOBILE_LOCATION, MOBILE_STRONGHOLD, splitLocation, territory as boardTerritory } from '../game/board';
|
|
import { FACTIONS } from '../game/catalog';
|
|
import { DISCOVERY_SPICE_CARDS } from '../game/discoveries';
|
|
import { LEADER_SKILL_CARDS } from '../game/leader-skill-cards';
|
|
import { nativeTypedClearBoard, nativeTypedPlace } from './fixture-discovery-native-typed';
|
|
import { openSkillsTechBattle } from './fixture-skills-tech-battle';
|
|
import {
|
|
advanceDiscoveryNativeE1Skills as advance, assertDiscoveryNativeE1SkillsCustody as custody,
|
|
createDiscoveryNativeE1SkillsBattle as battle, createDiscoveryNativeE1SkillsEntry as entry,
|
|
discoveryNativeE1PlanetologistRoute, discoveryNativeE1SkillsPlayer as player,
|
|
finishDiscoveryNativeE1SkillsBattle as finish, movementDiscoveryNativeE1Skills as movement,
|
|
phaseDiscoveryNativeE1Skills as phase, reloadDiscoveryNativeE1Skills as reload,
|
|
revealDiscoveryNativeE1SkillsBattle as reveal, settleDiscoveryNativeE1Skills as settle,
|
|
stepDiscoveryNativeE1Skills as step,
|
|
} from './fixture-discovery-native-e1-skills';
|
|
|
|
function reject(game: Game, actor: string, action: Action): void {
|
|
const before = reload(game);
|
|
assert.throws(() => applyAction(game, actor, action));
|
|
assert.deepEqual(game, before, 'Invalid native selections cannot partially debit or move physical inventory');
|
|
}
|
|
function acceptEntry(game: Game, source: string): Action {
|
|
assert.ok(game.decision?.kind === 'discoveryEntry');
|
|
return { type: 'decision', event: game.decision.event, accept: true,
|
|
groups: [{ source, normal: 1, elite: 1 }] };
|
|
}
|
|
|
|
void test('original E1 all14 offers precede native Traitors and preserve first-Storm Tech and real first-Mentat retained cards', () => {
|
|
for (const advanced of [false, true]) for (const tech of [false, true]) {
|
|
const f = entry({ advanced, tech, strongholds: advanced });
|
|
const skills = f.offered.leaderSkills!;
|
|
assert.deepEqual([...skills.deck, ...Object.values(skills.offers).flatMap(o => o.cards)].sort(), LEADER_SKILL_CARDS.map(c => c.id).sort());
|
|
assert.ok(f.offered.players.every(p => !p.traitorChoices.length && !p.traitors.length));
|
|
assert.equal(player(f.offered, f.opponent).faceDancers, undefined);
|
|
assert.equal(f.offered.discoveries!.tokens.length, 8);
|
|
assert.deepEqual(f.offered.spiceDeck.flatMap(c => 'territory' in c && c.discovery ? [c.discovery] : []).sort(),
|
|
DISCOVERY_SPICE_CARDS.map(c => c.discovery).sort());
|
|
assert.equal(f.offered.spiceDeck.filter(c => 'sandtrout' in c && c.sandtrout).length, 1);
|
|
assert.equal(player(f.afterSetup, f.opponent).faceDancers!.length, 3);
|
|
assert.equal(player(f.afterSetup, f.owner).forces[MOBILE_LOCATION], 6);
|
|
assert.equal(player(f.afterSetup, f.owner).elites!.forces[MOBILE_LOCATION], 3);
|
|
for (const p of f.afterSetup.players) assert.equal(p.spice, FACTIONS.find(c => c.id === p.faction)!.spice);
|
|
if (tech) {
|
|
assert.ok(Object.values(f.initial.techTokens!).every(t => t.owner === null));
|
|
assert.equal(f.afterFirstStorm.techTokens!.heighliners.owner, f.owner);
|
|
assert.equal(f.afterFirstStorm.techTokens!.axlotl.owner, f.opponent);
|
|
assert.equal(f.afterFirstStorm.techTokens!.production.owner, f.fremen);
|
|
}
|
|
if (advanced) {
|
|
assert.equal(f.beforeFirstMentat.strongholdCards!.owners.arrakeen, null);
|
|
const claimed = step(f.beforeFirstMentat, f.firstMentatStep);
|
|
assert.equal(claimed.turn, 2);
|
|
assert.equal(claimed.strongholdCards!.owners.arrakeen, f.owner);
|
|
assert.equal(claimed.strongholdCards!.owners[MOBILE_STRONGHOLD], f.owner);
|
|
assert.equal(f.game.strongholdCards!.owners.sietch_tabr, null);
|
|
}
|
|
custody(f.afterSetup); custody(f.game);
|
|
}
|
|
});
|
|
|
|
void test('authenticated original lobby and original undealt setup retain actual IDs and authoritative offers', () => {
|
|
const initial = createGame('HUMANE1DISCOVERY', newPlayer('human-ix', 'Human Ix', 'ixians'), true, ['ix']);
|
|
for (const [id, faction] of [['human-tl', 'tleilaxu'], ['human-gu', 'guild'], ['human-fr', 'fremen']] as const)
|
|
joinGame(initial, newPlayer(id, faction, faction));
|
|
const before = reload(initial), f = entry({ initial, tech: true, strongholds: true });
|
|
assert.deepEqual(initial, before);
|
|
assert.deepEqual(f.game.players.map(p => p.id), before.players.map(p => p.id));
|
|
assert.equal(f.owner, 'human-ix');
|
|
const saved = reload(f.offered), resumed = entry({ initial: saved, skill: 'suk-graduate' });
|
|
assert.deepEqual(saved, f.offered);
|
|
assert.deepEqual(resumed.offered.leaderSkills, saved.leaderSkills, 'Never redeal an authenticated offer');
|
|
assert.equal(resumed.trainer, f.trainer);
|
|
custody(resumed.game);
|
|
let randomLobby = reload(initial);
|
|
randomLobby.discoveryEnabled = true;
|
|
randomLobby = applyAction(randomLobby, randomLobby.host, { type: 'techTokens', enabled: true });
|
|
randomLobby = applyAction(randomLobby, randomLobby.host, { type: 'strongholdCards', enabled: true });
|
|
for (const p of randomLobby.players) randomLobby = applyAction(randomLobby, p.id, { type: 'ready' });
|
|
const randomSetup = initializeLeaderSkillsGameForAudit(randomLobby), originalOffers = reload(randomSetup);
|
|
const actual = entry({ initial: randomSetup });
|
|
assert.deepEqual(randomSetup, originalOffers);
|
|
assert.deepEqual(actual.offered.leaderSkills, originalOffers.leaderSkills);
|
|
const entered = applyAction(actual.game, actual.owner, acceptEntry(actual.game, actual.source));
|
|
assert.equal(player(entered, actual.owner).forces['shrine:0'], 2);
|
|
assert.equal(player(entered, actual.owner).elites!.forces['shrine:0'], 1);
|
|
custody(entered);
|
|
});
|
|
|
|
void test('trained original Ix typed free entry moves selected counters once without consuming training, shipment, movement, wallets or Tech', () => {
|
|
for (const advanced of [false, true]) {
|
|
const f = entry({ advanced, tech: true, strongholds: advanced });
|
|
const before = player(f.game, f.owner), action = acceptEntry(f.game, f.source);
|
|
assert.deepEqual(viewGame(f.game, f.owner).discoveryEntry!.sources, [{ source: f.source, normal: 2, elite: 1 }]);
|
|
reject(f.game, f.opponent, action);
|
|
reject(f.game, f.owner, { ...action, groups: [{ source: f.source, normal: 1, elite: 2 }] });
|
|
const arrived = applyAction(reload(f.game), f.owner, action), own = player(arrived, f.owner);
|
|
assert.equal(own.forces['shrine:0'], 2);
|
|
assert.equal(own.elites!.forces['shrine:0'], 1);
|
|
assert.equal(own.forces[f.source], 1);
|
|
assert.equal(own.elites!.forces[f.source] ?? 0, 0);
|
|
assert.equal(own.spice, before.spice);
|
|
assert.equal(own.reserves, before.reserves);
|
|
assert.equal(own.moved, before.moved);
|
|
assert.equal(own.shipped, before.shipped);
|
|
assert.deepEqual(arrived.leaderSkills, f.game.leaderSkills);
|
|
assert.deepEqual(arrived.techTokens, f.game.techTokens);
|
|
assert.equal(arrived.discoveryEntry, undefined);
|
|
reject(arrived, f.owner, action);
|
|
custody(arrived);
|
|
}
|
|
});
|
|
|
|
void test('original paid typed shipment and Planetologist three-territory nested route retain HMS identity and accrue only native Heighliners', () => {
|
|
for (const advanced of [false, true]) {
|
|
const f = entry({ advanced, tech: true, strongholds: advanced, skill: 'planetologist' });
|
|
assert.ok(f.game.decision?.kind === 'discoveryEntry');
|
|
let game = applyAction(f.game, f.owner, { type: 'decision', event: f.game.decision.event, accept: false });
|
|
game = movement(game, f.owner);
|
|
nativeTypedClearBoard(game);
|
|
const route = discoveryNativeE1PlanetologistRoute(game);
|
|
nativeTypedPlace(game, f.owner, route.source, 3, 1);
|
|
assert.equal(gameDistance(game, route.source, 'shrine:0', key => splitLocation(key).sector === game.storm), 3);
|
|
const hms = player(game, f.owner).forces[MOBILE_LOCATION], beforeShip = reload(game);
|
|
game = settle(applyAction(game, f.owner, { type: 'ship', territory: 'shrine', sector: 0, amount: 2, elite: 1 }));
|
|
assert.equal(player(game, f.owner).spice, player(beforeShip, f.owner).spice - 2);
|
|
assert.equal(player(game, f.guild).spice, player(beforeShip, f.guild).spice + 2);
|
|
assert.equal(player(game, f.owner).reserves, player(beforeShip, f.owner).reserves - 2);
|
|
assert.equal(player(game, f.owner).elites!.reserves, player(beforeShip, f.owner).elites!.reserves - 1);
|
|
const moving = reload(game), move: Action = { type: 'move', territory: 'shrine', sector: 0,
|
|
forces: { [route.source]: 2 }, eliteForces: { [route.source]: 1 }, planetologist: 'range' };
|
|
reject(game, f.owner, { ...move, planetologist: undefined });
|
|
game = settle(applyAction(game, f.owner, move));
|
|
assert.equal(player(game, f.owner).forces['shrine:0'], 4);
|
|
assert.equal(player(game, f.owner).elites!.forces['shrine:0'], 2);
|
|
assert.equal(player(game, f.owner).forces[route.source], 2);
|
|
assert.equal(player(game, f.owner).forces[MOBILE_LOCATION], hms);
|
|
assert.equal(player(game, f.owner).spice, player(moving, f.owner).spice);
|
|
assert.equal(player(game, f.owner).moved, 1);
|
|
assert.equal(game.techTokens!.heighliners.spice, 1);
|
|
const wallet = player(game, f.owner).spice;
|
|
game = advance(game, g => g.phase !== 5);
|
|
assert.equal(game.techTokens!.heighliners.spice, 0);
|
|
assert.equal(player(game, f.owner).spice, wallet + 1);
|
|
assert.deepEqual(game.leaderSkills, moving.leaderSkills);
|
|
custody(game);
|
|
}
|
|
});
|
|
|
|
void test('original trained Suk settles fixed physical rescue before only genuine Cyborg Tank losses can be substituted inside Shrine', () => {
|
|
for (const advanced of [false, true]) for (const tech of [false, true]) {
|
|
const f = battle({ advanced, tech, strongholds: advanced });
|
|
const revealed = reveal(f), pending = finish(revealed, 'rescue');
|
|
assert.ok(pending.decision?.kind === 'sukRescue');
|
|
const own = player(pending, f.owner);
|
|
assert.deepEqual(pending.pendingSukRescue!.losses, { normal: 4, elite: 2, paidNormal: 0, paidElite: advanced ? 1 : 0 });
|
|
assert.equal(own.tanks, 0);
|
|
assert.equal(own.forces[f.location], 8, 'Counters remain physically committed until the real rescue');
|
|
assert.equal(pending.pendingIxSubstitution, undefined);
|
|
assert.equal(own.spice, player(revealed, f.owner).spice - (advanced ? 1 : 0) +
|
|
player(revealed, f.opponent).leaders.find(l => l.id === f.losingLeader)!.strength,
|
|
'Retained Arrakeen cannot subsidize a nested site');
|
|
const choice = pending.decision.options.findIndex(o => o.normal === 2 && o.elite === 1 && o.kept?.kind === 'normal');
|
|
assert.ok(choice >= 0);
|
|
reject(pending, f.owner, { type: 'decision', sources: { [f.location]: 1 }, recover: { [f.location]: 1 } });
|
|
const rescueAction: Action = { type: 'decision', event: pending.decision.event, choice };
|
|
const rescued = applyAction(reload(pending), f.owner, rescueAction), after = player(rescued, f.owner);
|
|
assert.equal(rescued.decision?.kind, 'ixSubstitution');
|
|
assert.equal(rescued.lastBattleContext!.sukRescue!.completed, true);
|
|
assert.equal(after.forces[f.location], 3);
|
|
assert.equal(after.elites!.forces[f.location] ?? 0, 0);
|
|
assert.equal(after.reserves, own.reserves + 2);
|
|
assert.equal(after.elites!.reserves, own.elites!.reserves + 1);
|
|
assert.equal(after.tanks, 3);
|
|
assert.equal(after.elites!.tanks, 1);
|
|
assert.deepEqual(rescued.pendingIxSubstitution!.losses, { [f.location]: 1 });
|
|
reject(rescued, f.owner, rescueAction);
|
|
reject(rescued, f.owner, { type: 'decision', sources: { [f.location]: 1 }, recover: { [f.replacementSource]: 1 } });
|
|
reject(rescued, f.owner, { type: 'decision', sources: { [f.location]: 2 }, recover: { [f.location]: 2 } });
|
|
const substituted = settle(applyAction(reload(rescued), f.owner, { type: 'decision',
|
|
sources: { [f.location]: 1 }, recover: { [f.location]: 1 } }));
|
|
assert.equal(player(substituted, f.owner).forces[f.location], 3);
|
|
assert.equal(player(substituted, f.owner).elites!.forces[f.location], 1);
|
|
assert.equal(player(substituted, f.owner).tanks, 3);
|
|
assert.equal(player(substituted, f.owner).elites!.tanks, 0);
|
|
assert.equal(player(substituted, f.owner).spice, after.spice);
|
|
assert.equal(substituted.lastBattleContext!.ixSubstitution!.completed, true);
|
|
custody(substituted);
|
|
}
|
|
});
|
|
|
|
void test('green original Ix Planetologist waits through native substitution then disposes exactly once before optional winner cleanup and Tech', () => {
|
|
for (const advanced of [false, true]) {
|
|
const f = battle({ advanced, tech: true, strongholds: advanced, skill: 'planetologist' });
|
|
const pending = finish(reveal(f), 'substitution');
|
|
assert.equal(pending.decision?.kind, 'ixSubstitution');
|
|
assert.ok(player(pending, f.owner).hand.some(c => c.id === f.weapon));
|
|
assert.ok(pending.pendingWinnerDiscards!.cards.includes(f.weapon));
|
|
assert.equal(pending.techTokens!.axlotl.owner, f.opponent);
|
|
const cards = finish(applyAction(reload(pending), f.owner, { type: 'decision', decline: true }), 'cards');
|
|
assert.equal(cards.decision?.kind, 'battleCards');
|
|
assert.equal(cards.discard.filter(c => c.id === f.weapon).length, 1);
|
|
assert.equal(player(cards, f.owner).hand.some(c => c.id === f.weapon), false);
|
|
assert.equal(cards.lastBattleContext!.winnerDiscards!.completed, true);
|
|
assert.equal(player(cards, f.owner).forces[f.location], 2);
|
|
assert.equal(player(cards, f.owner).tanks, 6);
|
|
reject(cards, f.owner, { type: 'decision', discard: [f.weapon] });
|
|
const face = finish(applyAction(cards, f.owner, { type: 'decision', discard: [f.defense] }), 'faceDance');
|
|
assert.equal(face.techTokens!.axlotl.owner, f.owner);
|
|
assert.equal(face.discard.filter(c => c.id === f.weapon).length, 1);
|
|
assert.equal(face.discard.filter(c => c.id === f.defense).length, 1);
|
|
assert.equal(face.discard.filter(c => c.id === f.losingCard).length, 1);
|
|
assert.equal(face.decision?.kind, 'faceDance');
|
|
custody(face);
|
|
}
|
|
});
|
|
|
|
void test('matching native Face Dance kills the original trainer after rescue/cards/Tech, returns training once and retains native replacement eligibility', () => {
|
|
for (const advanced of [false, true]) {
|
|
const f = battle({ advanced, tech: true, strongholds: advanced });
|
|
const cards = finish(reveal(f), 'cards');
|
|
assert.equal(cards.decision?.kind, 'battleCards');
|
|
const face = finish(applyAction(cards, f.owner, { type: 'decision', discard: [f.defense] }), 'faceDance');
|
|
assert.equal(face.lastBattleContext!.winner, f.owner);
|
|
assert.equal(face.techTokens!.axlotl.owner, f.owner);
|
|
assert.ok(player(face, f.owner).hand.some(c => c.id === f.weapon));
|
|
const own = player(face, f.owner), dancer = player(face, f.opponent);
|
|
const action: Action = { type: 'decision', reveal: true, sector: 0,
|
|
sources: { reserves: 1, [f.replacementSource]: 2 } };
|
|
reject(face, f.opponent, { ...action, sector: 1 });
|
|
const danced = applyAction(reload(face), f.opponent, action);
|
|
assert.equal(player(danced, f.owner).leaders.find(l => l.id === f.trainer)!.dead, true);
|
|
assert.equal(danced.leaderSkills!.assignments.some(a => a.leader === f.trainer), false);
|
|
assert.equal(danced.leaderSkills!.deck.filter(s => s === f.skill).length, 1);
|
|
assert.equal(player(danced, f.owner).reserves, own.reserves + 3);
|
|
assert.equal(player(danced, f.owner).forces[f.location] ?? 0, 0);
|
|
assert.equal(player(danced, f.opponent).forces[f.location], 3);
|
|
assert.equal(player(danced, f.opponent).forces[f.replacementSource], 1);
|
|
assert.equal(player(danced, f.opponent).reserves, dancer.reserves - 1);
|
|
for (const p of face.players) assert.equal(player(danced, p.id).spice, p.spice, 'No second death bounty, subsidy or support debit');
|
|
assert.deepEqual(danced.techTokens, face.techTokens);
|
|
assert.deepEqual(player(danced, f.owner).hand, own.hand);
|
|
reject(danced, f.opponent, action);
|
|
const mentat = phase(danced, 8), stock = player(mentat, f.opponent).faceDancers!;
|
|
const revealedDancer = stock.find(c => c.leader === f.trainer); assert.ok(revealedDancer?.revealed);
|
|
reject(mentat, f.opponent, { type: 'replaceFaceDancer', leader: f.trainer });
|
|
const untouched = stock.find(c => !c.revealed); assert.ok(untouched);
|
|
const stockBefore = stock.map(c => ({ ...c }));
|
|
const replaced = settle(applyAction(mentat, f.opponent, { type: 'replaceFaceDancer', leader: untouched.leader }));
|
|
assert.equal(player(replaced, f.opponent).faceDancerReplacedTurn, replaced.turn);
|
|
assert.deepEqual(player(replaced, f.opponent).faceDancers!.filter(c => c.leader === f.trainer), [revealedDancer]);
|
|
for (const c of stockBefore.filter(c => c.leader !== untouched.leader))
|
|
assert.ok(player(replaced, f.opponent).faceDancers!.some(n => n.leader === c.leader && n.revealed === c.revealed));
|
|
reject(replaced, f.opponent, { type: 'replaceFaceDancer', leader: player(replaced, f.opponent).faceDancers!.find(c => !c.revealed)!.leader });
|
|
custody(replaced);
|
|
if (!advanced) {
|
|
const revival = advance(replaced, g => g.turn === 3 && g.phase === 4 && !g.phaseOpening && !g.response && !g.decision);
|
|
assert.equal(player(revival, f.owner).leaders.find(l => l.id === f.trainer)!.dead, true);
|
|
reject(revival, f.opponent, { type: 'reviveForeignGhola', leader: f.trainer });
|
|
assert.equal(revival.leaderSkills!.assignments.some(a => a.leader === f.trainer), false,
|
|
'Basic native Tleilaxu cannot turn a matching dead trainer into a foreign skill carrier');
|
|
custody(revival);
|
|
}
|
|
}
|
|
});
|
|
|
|
void test('all four original minimal policies produce real typed entry and capped trained rescue outcomes rather than action-list echoes', () => {
|
|
const f = entry({ advanced: true, tech: true, strongholds: true });
|
|
const b = battle({ advanced: true, tech: true, strongholds: true }), pending = finish(reveal(b), 'rescue');
|
|
assert.ok(pending.decision?.kind === 'sukRescue');
|
|
assert.equal(DIFFICULTIES.length, 4);
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const entryView = viewGame(f.game, f.owner); entryView.players.find(p => p.id === f.owner)!.bot = difficulty;
|
|
const entryAction = botActions(entryView)[0]; assert.ok(entryAction);
|
|
const entered = applyAction(reload(f.game), f.owner, entryAction), before = player(f.game, f.owner), own = player(entered, f.owner);
|
|
assert.equal(entered.discoveryEntry, undefined);
|
|
const arrived = own.forces['shrine:0'] ?? 0;
|
|
assert.equal((own.forces[f.source] ?? 0) + arrived, before.forces[f.source]);
|
|
assert.equal((own.elites!.forces[f.source] ?? 0) + (own.elites!.forces['shrine:0'] ?? 0), 1);
|
|
assert.equal(own.spice, before.spice);
|
|
assert.equal(own.moved, before.moved);
|
|
assert.equal(own.shipped, before.shipped);
|
|
const rescueView = viewGame(pending, b.owner); rescueView.players.find(p => p.id === b.owner)!.bot = difficulty;
|
|
const rescueAction = botActions(rescueView)[0]; assert.ok(rescueAction);
|
|
const rescued = applyAction(reload(pending), b.owner, rescueAction);
|
|
assert.equal(rescued.pendingSukRescue, null);
|
|
assert.equal(rescued.lastBattleContext!.sukRescue!.completed, true);
|
|
assert.equal(player(rescued, b.owner).tanks, 3, `${difficulty}: exactly three of six committed casualties are saved`);
|
|
assert.equal(player(rescued, b.owner).forces[b.location], 3, `${difficulty}: exactly one rescue is kept in the original nested sector`);
|
|
assert.equal(player(rescued, b.owner).reserves, player(pending, b.owner).reserves + 2);
|
|
assert.equal(player(rescued, b.owner).spice, player(pending, b.owner).spice);
|
|
const completed = finish(rescued);
|
|
assert.equal(completed.lastBattleContext!.winner, b.owner);
|
|
assert.equal(player(completed, b.opponent).tanks, 8);
|
|
assert.equal(completed.techTokens!.axlotl.owner, b.owner);
|
|
custody(entered); custody(completed);
|
|
}
|
|
});
|
|
|
|
void test('hidden native Smuggler policy seals a real legal trainer plan in Discovery without applying full-state mode rules to a private player view', () => {
|
|
const f = entry({ advanced: true, tech: true, strongholds: true, skill: 'smuggler' });
|
|
let game = movement(f.game, f.owner);
|
|
nativeTypedClearBoard(game);
|
|
for (const seat of game.players) { game.deck.push(...seat.hand); seat.hand = []; }
|
|
const territory = 'habbanya_ridge_sietch', key = `${territory}:${boardTerritory(territory).sectors[0]}`;
|
|
nativeTypedPlace(game, f.owner, key, 3);
|
|
nativeTypedPlace(game, f.opponent, key, 3);
|
|
game = openSkillsTechBattle(phase(game, 6), f.owner, f.opponent, territory, true);
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const view = viewGame(game, f.owner);
|
|
view.players.find(p => p.id === f.owner)!.bot = difficulty;
|
|
const plan = botActions(view).find(action => action.type === 'battlePlan' && action.leader === f.trainer);
|
|
assert.ok(plan, `${difficulty} must retain the actually available hidden trainer as a legal plan candidate.`);
|
|
const committed = applyAction(reload(game), f.owner, plan);
|
|
assert.equal(committed.battle!.plans[f.owner].leader, f.trainer);
|
|
assert.equal(player(committed, f.owner).forces[key], 3);
|
|
assert.equal(player(committed, f.owner).spice, player(game, f.owner).spice,
|
|
'A sealed first plan does not debit support or collect unearned Smuggler spice.');
|
|
}
|
|
});
|