335 lines
22 KiB
TypeScript
335 lines
22 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { pinDeterministicRandom } from './deterministic-random';
|
|
import { applyAction, handLimit, normalizeAutomaticGame, viewGame, type Action, type Game } from '../game/engine';
|
|
import { botActions } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import { treacheryDeck, spiceDeck } from '../game/cards';
|
|
import { richeseCards } from '../game/richese-cards';
|
|
import { traitorDeck } from '../game/traitors';
|
|
import { DISCOVERY_SPICE_CARDS, validateDiscoveryState } from '../game/discoveries';
|
|
import { validateLeaderSkills } from '../game/leader-skills';
|
|
import { validateRicheseNoField } from '../game/richese-no-field';
|
|
import { createTechTokens, ownedTech, TECH_TOKENS } from '../game/tech-tokens';
|
|
import { strongholdBenefit, strongholdControllers } from '../game/stronghold-cards';
|
|
import { canUseAsKaramaRole, canUseAsTruthtranceRole } from '../game/shrine';
|
|
import {
|
|
advanceDiscoveryNativeE2Skills, completeDiscoveryNativeE2SkillsSetup, createDiscoveryNativeE2SkillsFixture,
|
|
discoveryNativeE2SkillsInvoiceAction, discoveryNativeE2SkillsMovementWindow,
|
|
discoveryNativeE2SkillsPlayer as player, finishDiscoveryNativeE2SkillsBattle,
|
|
holdDiscoveryNativeE2SkillsCard, prepareDiscoveryNativeE2SkillsMarkerBattle,
|
|
revealDiscoveryNativeE2Skills, settleDiscoveryNativeE2Skills,
|
|
} from './fixture-discovery-native-e2-skills';
|
|
|
|
const reload = (game: Game): Game => JSON.parse(JSON.stringify(game));
|
|
function reject(game: Game, actor: string, action: Action): void {
|
|
const before = structuredClone(game);
|
|
assert.throws(() => applyAction(game, actor, action));
|
|
assert.deepEqual(game, before);
|
|
}
|
|
function custody(game: Game): void {
|
|
const cards = [...game.deck, ...game.discard, ...game.players.flatMap(p => p.hand),
|
|
...(game.richeseCache ?? []), ...(game.richeseRemoved ?? [])];
|
|
assert.deepEqual(cards.map(card => card.id).sort(),
|
|
[...treacheryDeck(game.expansions), ...richeseCards()].map(card => card.id).sort());
|
|
assert.equal(new Set(cards.map(card => card.id)).size, cards.length);
|
|
for (const p of game.players) {
|
|
assert.equal(p.reserves + p.tanks + Object.values(p.forces).reduce((total, amount) => total + amount, 0), 20);
|
|
if (p.noField) validateRicheseNoField(p.noField);
|
|
}
|
|
validateLeaderSkills(game.leaderSkills!, game.players);
|
|
validateDiscoveryState(game.discoveries!);
|
|
if (game.status === 'playing') {
|
|
const held = [...(game.traitorReserve ?? []), ...game.players.flatMap(p => [...p.traitors, ...p.traitorChoices])];
|
|
assert.deepEqual(held.sort(), traitorDeck(game.players, game.expansions.includes('ix')).sort());
|
|
assert.equal(new Set(held).size, held.length);
|
|
}
|
|
}
|
|
|
|
// Source composition: LEADER_SKILLS_RUNTIME.md native Richese/Advanced CHOAM,
|
|
// CHOAM_LEADER_SKILLS.md, DISCOVERY_COMPONENTS.md and original native marker
|
|
// fixture. No Banker income, Mentat questions or mixed No-Field interpretation.
|
|
for (const advanced of [false, true]) for (const tech of [false, true]) {
|
|
void test(`${advanced ? 'Advanced' : 'Basic'} original all14 E2/Discovery preserves independent ordinary/cache custody with ${tech ? 'native Tech' : 'no Tech'}`, () => {
|
|
const f = createDiscoveryNativeE2SkillsFixture({ advanced, tech, strongholds: advanced });
|
|
const ordinary = [...f.setup.deck, ...f.setup.players.flatMap(p => p.hand)];
|
|
assert.equal(ordinary.length, 35);
|
|
assert.deepEqual(ordinary.map(c => c.id).sort(), treacheryDeck(['choam']).map(c => c.id).sort());
|
|
assert.deepEqual(f.setup.richeseCache!.map(c => c.id).sort(), richeseCards().map(c => c.id).sort());
|
|
assert.equal(f.setup.richeseCache!.length, 10);
|
|
assert.equal(f.setup.leaderSkills!.deck.length + Object.values(f.setup.leaderSkills!.offers).reduce((n, o) => n + o.cards.length, 0), 14);
|
|
assert.ok(f.setup.players.every(p => p.traitors.length === 0 && p.traitorChoices.length === 0));
|
|
assert.equal(f.setup.spiceDeck.length, spiceDeck().length + 7);
|
|
assert.deepEqual(f.setup.spiceDeck.flatMap(c => 'territory' in c && c.discovery ? [c.discovery] : []).sort(),
|
|
DISCOVERY_SPICE_CARDS.map(c => c.discovery).sort());
|
|
assert.equal(f.setup.discoveries!.tokens.length, 8);
|
|
assert.equal(viewGame(f.setup, f.richese).leaderSkills!.offer!.cards.includes('planetologist'), true);
|
|
assert.equal(viewGame(f.setup, f.choam).leaderSkills!.offer!.cards.includes('smuggler'), true);
|
|
assert.ok(player(f.game, f.richese).hand.some(c => c.id === f.green));
|
|
assert.equal(f.game.richeseCache!.some(c => c.id === f.green), false);
|
|
assert.ok(f.game.richeseCache!.length > 0, 'Two-turn bounded recipes do not bypass the exhausted-cache ruling.');
|
|
if (tech) {
|
|
assert.deepEqual(f.setup.techTokens, createTechTokens());
|
|
assert.ok(TECH_TOKENS.every(token => f.afterFirstStorm.techTokens![token.id].owner !== null));
|
|
assert.ok(TECH_TOKENS.every(token => f.afterFirstStorm.players.some(p => p.id === f.afterFirstStorm.techTokens![token.id].owner)));
|
|
} else assert.equal(f.afterFirstStorm.techTokens, undefined);
|
|
assert.deepEqual(f.afterSetup.leaderSkills!.assignments, f.game.leaderSkills!.assignments);
|
|
custody(f.setup);
|
|
custody(f.game);
|
|
});
|
|
}
|
|
|
|
void test('actual original offers can train and finish native setup without a controlled shuffle or demanded skill', () => {
|
|
const f = createDiscoveryNativeE2SkillsFixture({ advanced: true, kind: 'stash', useActualOffers: true });
|
|
const offered = structuredClone(f.setup.leaderSkills!.offers);
|
|
const after = completeDiscoveryNativeE2SkillsSetup(reload(f.setup), true);
|
|
for (const assignment of after.leaderSkills!.assignments) {
|
|
assert.ok(offered[assignment.owner].cards.includes(assignment.skill));
|
|
assert.ok(player(f.setup, assignment.owner).leaders.some(l => l.id === assignment.leader));
|
|
}
|
|
assert.equal(after.leaderSkills!.assignments.length, after.players.length);
|
|
assert.deepEqual(after.players.flatMap(p => p.hand).map(c => c.id).sort(), f.setup.players.flatMap(p => p.hand).map(c => c.id).sort());
|
|
assert.equal(Object.keys(after.leaderSkills!.offers).length, 0);
|
|
custody(after);
|
|
custody(f.game);
|
|
});
|
|
|
|
for (const advanced of [false, true]) void test(`${advanced ? 'Advanced' : 'Basic'} CHOAM skill owner opens the native five-card Stash boundary, then every minimal policy disposes its actual sixth card once`, () => {
|
|
const f = createDiscoveryNativeE2SkillsFixture({ advanced, kind: 'stash', tech: true, strongholds: advanced });
|
|
const before = f.game, choam = player(before, f.choam);
|
|
assert.equal(handLimit(choam), 5);
|
|
while (choam.hand.length < handLimit(choam)) {
|
|
const card = before.deck.shift();
|
|
assert.ok(card); choam.hand.push(card);
|
|
}
|
|
f.staging.push('Conserved original undealt ordinary cards into CHOAM hand at its native five-card capacity.');
|
|
const wallet = choam.spice, hand = choam.hand.map(c => c.id), drawn = before.deck[0];
|
|
assert.ok(drawn);
|
|
const revealed = revealDiscoveryNativeE2Skills(f);
|
|
assert.equal(revealed.decision?.kind, 'discoveryDiscard');
|
|
assert.equal(revealed.decision?.player, f.choam);
|
|
assert.deepEqual(player(revealed, f.choam).hand.map(c => c.id), [...hand, drawn.id]);
|
|
assert.equal(player(revealed, f.choam).spice, wallet);
|
|
assert.equal(revealed.deck.length, before.deck.length - 1);
|
|
assert.equal(viewGame(revealed, f.choam).players.find(p => p.id === f.choam)!.hand!.length, 6);
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const view = viewGame(revealed, f.choam);
|
|
view.players.find(p => p.id === f.choam)!.bot = difficulty;
|
|
const action = botActions(view).find(a => a.type === 'decision' && typeof a.card === 'string');
|
|
assert.ok(action, `${difficulty} must return a real owned disposal action.`);
|
|
const done = settleDiscoveryNativeE2Skills(applyAction(reload(revealed), f.choam, action));
|
|
assert.equal(player(done, f.choam).hand.length, 5);
|
|
assert.equal(player(done, f.choam).spice, wallet);
|
|
assert.equal(player(done, f.choam).hand.some(c => c.id === action.card), false);
|
|
assert.equal(done.discard.filter(c => c.id === action.card).length, 1);
|
|
assert.equal(done.discoveryStash!.stage, 'complete');
|
|
assert.equal(done.discoveries!.tokens.find(t => t.id === f.token)!.status, 'removed');
|
|
assert.deepEqual(done.leaderSkills!.assignments, revealed.leaderSkills!.assignments);
|
|
reject(done, f.choam, action);
|
|
reject(done, f.choam, { type: 'discovery', token: f.token, reveal: true });
|
|
custody(done);
|
|
}
|
|
for (const physical of player(revealed, f.choam).hand) {
|
|
const action: Action = { type: 'decision', event: revealed.discoveryStash!.event, card: physical.id };
|
|
reject(revealed, f.richese, action);
|
|
reject(revealed, f.choam, { ...action, event: 'stale-stash' });
|
|
const done = settleDiscoveryNativeE2Skills(applyAction(reload(revealed), f.choam, action));
|
|
assert.equal(done.discard.filter(c => c.id === physical.id).length, 1);
|
|
assert.equal(player(done, f.choam).hand.length, 5);
|
|
assert.equal(done.deck.length, before.deck.length - 1);
|
|
custody(done);
|
|
}
|
|
custody(revealed);
|
|
});
|
|
|
|
for (const advanced of [false, true]) void test(`${advanced ? 'Advanced' : 'Basic'} original first end-Mentat claims and source-local Shrine/Stronghold entitlement precede trained CHOAM invoice and Bureaucrat diversion`, () => {
|
|
const f = createDiscoveryNativeE2SkillsFixture({ advanced, kind: 'invoice', tech: true, strongholds: advanced });
|
|
const checkpoint = discoveryNativeE2SkillsMovementWindow(f, f.choam), before = checkpoint.game;
|
|
assert.deepEqual(f.setup.techTokens, createTechTokens());
|
|
assert.ok(TECH_TOKENS.every(t => f.afterFirstStorm.techTokens![t.id].owner !== null));
|
|
if (advanced) {
|
|
assert.equal(checkpoint.beforeFirstMentat.strongholdCards!.claimedTurn, 0);
|
|
assert.equal(checkpoint.afterFirstMentat.strongholdCards!.claimedTurn, 1);
|
|
const expected = strongholdControllers(checkpoint.beforeFirstMentat.players, false);
|
|
assert.deepEqual(checkpoint.afterFirstMentat.strongholdCards!.owners, expected);
|
|
const claims = Object.entries(expected).filter(([, owner]) => owner);
|
|
assert.ok(claims.length > 0);
|
|
for (const [territory, owner] of claims) {
|
|
assert.equal(strongholdBenefit(checkpoint.afterFirstMentat.strongholdCards, owner!, territory), territory);
|
|
assert.equal(strongholdBenefit(checkpoint.afterFirstMentat.strongholdCards, owner!, 'shrine'), null);
|
|
assert.equal(strongholdBenefit(checkpoint.afterFirstMentat.strongholdCards, f.observer === owner ? f.choam : f.observer, territory), null);
|
|
}
|
|
}
|
|
assert.equal(player(before, f.guild).forces['shrine:0'], 1);
|
|
const truth = holdDiscoveryNativeE2SkillsCard(before, f.guild, c => c.effect === 'truthtrance', f.staging);
|
|
const karama = holdDiscoveryNativeE2SkillsCard(before, f.guild, c => c.effect === 'karama', f.staging);
|
|
assert.equal(canUseAsKaramaRole(before, player(before, f.guild), truth), true);
|
|
assert.equal(canUseAsTruthtranceRole(before, player(before, f.guild), karama), true);
|
|
assert.equal(canUseAsKaramaRole(before, player(before, f.choam), truth), false);
|
|
assert.equal(canUseAsTruthtranceRole(before, player(before, f.choam), karama), false);
|
|
const invoice = discoveryNativeE2SkillsInvoiceAction(before);
|
|
const own = structuredClone(player(before, f.choam)), recipient = player(before, f.guild).spice;
|
|
let paid = applyAction(reload(before), f.choam, invoice.action);
|
|
paid = advanceDiscoveryNativeE2Skills(paid, g => g.decision?.kind === 'bureaucratPayment');
|
|
assert.ok(paid.decision?.kind === 'bureaucratPayment');
|
|
const pending = paid.bureaucratPayments!.pending!;
|
|
assert.deepEqual({ kind: pending.source.kind, payer: pending.source.payer, payee: pending.source.payee, amount: pending.source.amount },
|
|
{ kind: 'shipment', payer: f.choam, payee: f.guild, amount: 5 });
|
|
assert.equal(pending.owner, f.observer);
|
|
assert.equal(player(paid, f.choam).spice, own.spice - 5);
|
|
assert.equal(player(paid, f.choam).reserves, own.reserves - 6);
|
|
assert.equal(player(paid, f.choam).forces[invoice.key], 6);
|
|
assert.equal(player(paid, f.guild).spice, recipient);
|
|
for (const redirect of [false, true]) {
|
|
const decision: Action = { type: 'decision', event: paid.decision!.event, redirect };
|
|
reject(paid, f.choam, decision);
|
|
reject(paid, f.observer, { ...decision, event: 'stale-payment' });
|
|
const done = settleDiscoveryNativeE2Skills(applyAction(reload(paid), f.observer, decision));
|
|
assert.equal(player(done, f.choam).spice, own.spice - 5);
|
|
assert.equal(player(done, f.guild).spice, recipient + (redirect ? 3 : 5));
|
|
assert.equal(player(done, f.observer).spice, player(paid, f.observer).spice);
|
|
assert.equal(done.bureaucratPayments!.used.length, redirect ? 1 : 0);
|
|
assert.equal(done.bureaucratPayments!.pending, undefined);
|
|
assert.equal(done.techTokens!.heighliners.spice, paid.techTokens!.heighliners.spice);
|
|
reject(done, f.observer, decision);
|
|
custody(done);
|
|
}
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const view = viewGame(paid, f.observer);
|
|
view.players.find(p => p.id === f.observer)!.bot = difficulty;
|
|
const action = botActions(view).find(a => a.type === 'decision' && typeof a.redirect === 'boolean');
|
|
assert.ok(action);
|
|
const done = settleDiscoveryNativeE2Skills(applyAction(reload(paid), f.observer, action));
|
|
assert.equal(player(done, f.guild).spice, recipient + (action.redirect ? 3 : 5));
|
|
assert.equal(player(done, f.choam).forces[invoice.key], 6);
|
|
assert.equal(player(done, f.choam).spice, own.spice - 5);
|
|
custody(done);
|
|
}
|
|
});
|
|
|
|
for (const advanced of [false, true]) void test(`${advanced ? 'Advanced' : 'Basic'} actual CHOAM Kulon fuel preserves the trained invoice, discards once and grants its native movement bonus`, () => {
|
|
const f = createDiscoveryNativeE2SkillsFixture({ advanced, kind: 'invoice' });
|
|
const checkpoint = discoveryNativeE2SkillsMovementWindow(f, f.choam);
|
|
const before = checkpoint.game;
|
|
const fuel = holdDiscoveryNativeE2SkillsCard(before, f.choam, c => c.name === 'Kulon' && c.kind === 'worthless', f.staging);
|
|
// Keep a real cancellation opportunity open to observe held fuel, not a fake receipt.
|
|
holdDiscoveryNativeE2SkillsCard(before, f.observer, c => c.effect === 'karama', f.staging);
|
|
const action: Action = { type: 'card', mode: 'choam', card: fuel.id };
|
|
const wallet = player(before, f.choam).spice, reserves = player(before, f.choam).reserves;
|
|
const requested = applyAction(reload(before), f.choam, action);
|
|
assert.equal(requested.response?.kind, 'choamWorthless');
|
|
assert.deepEqual(player(requested, f.choam).hand.find(c => c.id === fuel.id), fuel);
|
|
assert.equal(requested.discard.some(c => c.id === fuel.id), false);
|
|
const done = settleDiscoveryNativeE2Skills(requested);
|
|
assert.equal(done.discard.filter(c => c.id === fuel.id).length, 1);
|
|
assert.equal(player(done, f.choam).hand.some(c => c.id === fuel.id), false);
|
|
assert.equal(done.choamMovement!.bonus, 1);
|
|
assert.equal(player(done, f.choam).spice, wallet);
|
|
assert.equal(player(done, f.choam).reserves, reserves);
|
|
assert.deepEqual(done.leaderSkills!.assignments, before.leaderSkills!.assignments);
|
|
reject(done, f.choam, action);
|
|
const invoice = discoveryNativeE2SkillsInvoiceAction(done);
|
|
const paid = settleDiscoveryNativeE2Skills(applyAction(reload(done), f.choam, invoice.action));
|
|
assert.equal(player(paid, f.choam).spice, wallet - 5);
|
|
assert.equal(player(paid, f.choam).reserves, reserves - 6);
|
|
assert.equal(player(paid, f.choam).forces[invoice.key], 6);
|
|
assert.equal(paid.choamMovement!.bonus, 1);
|
|
custody(paid);
|
|
});
|
|
|
|
for (const advanced of [false, true]) for (const marker of [0, 3, 5] as const) {
|
|
void test(`${advanced ? 'Advanced' : 'Basic'} nested marker-only ${marker} keeps paid custody until both plans then applies capped physical reveal and canonical green Planetologist strength`, () => {
|
|
const f = createDiscoveryNativeE2SkillsFixture({ advanced, tech: true, strongholds: advanced });
|
|
const battle = prepareDiscoveryNativeE2SkillsMarkerBattle(f, { marker, ...(marker === 5 ? { reserveCap: 3 } : {}) });
|
|
const before = player(battle.beforeShipment, f.richese), shipped = player(battle.afterShipment, f.richese);
|
|
assert.equal(shipped.spice, before.spice - 1, 'Original Discovery locations use the stronghold shipment price, independently of victory eligibility.');
|
|
assert.equal(shipped.reserves, before.reserves);
|
|
assert.deepEqual(shipped.forces, before.forces);
|
|
assert.deepEqual(shipped.noField!.tokens, before.noField!.tokens);
|
|
assert.deepEqual(shipped.noField!.deployed!.location, { territory: 'shrine', sector: 0 });
|
|
assert.equal(shipped.noField!.deployed!.tokenId, battle.token);
|
|
const privateOpponent = viewGame(battle.game, f.guild);
|
|
assert.equal(privateOpponent.richeseNoField!.private, null);
|
|
assert.equal(privateOpponent.richeseNoField!.public.deployed!.effectiveForces, 1);
|
|
reject(battle.game, f.richese, { ...battle.ownerPlan, dial: battle.materialized + 1, support: advanced ? battle.materialized + 1 : 0 });
|
|
let game = applyAction(reload(battle.game), f.richese, battle.ownerPlan);
|
|
assert.ok(player(game, f.richese).noField!.deployed);
|
|
assert.equal(player(game, f.richese).forces[battle.key] ?? 0, 0);
|
|
assert.equal(player(game, f.richese).reserves, shipped.reserves);
|
|
assert.ok(player(game, f.richese).hand.some(c => c.id === f.green));
|
|
game = applyAction(reload(game), f.guild, battle.opponentPlan);
|
|
assert.equal(player(game, f.richese).noField!.deployed, null);
|
|
assert.equal(player(game, f.richese).forces[battle.key] ?? 0, battle.materialized);
|
|
assert.equal(player(game, f.richese).reserves, shipped.reserves - battle.materialized);
|
|
assert.deepEqual(player(game, f.richese).noField!.tokens, before.noField!.tokens);
|
|
game = finishDiscoveryNativeE2SkillsBattle(reload(game), 'cleanup');
|
|
assert.equal(game.lastBattleContext!.winner, f.richese);
|
|
const trainer = player(battle.game, f.richese).leaders.find(l => l.id === f.trainers.richese)!;
|
|
assert.equal(player(game, f.richese).leaders.find(l => l.id === trainer.id)!.dead, false);
|
|
assert.equal(player(game, f.guild).leaders.find(l => l.id === battle.opponentPlan.leader)!.dead, false);
|
|
assert.equal(player(game, f.richese).tanks, before.tanks);
|
|
assert.equal(player(game, f.guild).tanks, player(battle.game, f.guild).tanks + 8);
|
|
assert.equal(player(game, f.richese).spice, player(battle.game, f.richese).spice, 'Zero support and inert green substitution charge no native fee or death bounty.');
|
|
assert.equal(game.discard.filter(c => c.id === f.green).length, 1);
|
|
assert.equal(player(game, f.richese).hand.some(c => c.id === f.green), false);
|
|
const noGreenPlan = { ...battle.ownerPlan };
|
|
delete noGreenPlan.weapon;
|
|
let ordinary = applyAction(reload(battle.game), f.richese, noGreenPlan);
|
|
ordinary = applyAction(ordinary, f.guild, battle.opponentPlan);
|
|
ordinary = finishDiscoveryNativeE2SkillsBattle(ordinary, 'cleanup');
|
|
assert.equal(ordinary.lastBattleContext!.winner, f.guild, 'Removing only the physical green substitute loses the same real battle.');
|
|
assert.equal(player(ordinary, f.richese).hand.some(c => c.id === f.green), true);
|
|
assert.equal(ordinary.discard.some(c => c.id === f.green), false);
|
|
custody(ordinary);
|
|
const loserTokens = ownedTech(battle.game.techTokens, f.guild);
|
|
const done = finishDiscoveryNativeE2SkillsBattle(reload(game));
|
|
if (loserTokens.length) {
|
|
const changed = TECH_TOKENS.filter(t => battle.game.techTokens![t.id].owner !== done.techTokens![t.id].owner);
|
|
assert.equal(changed.length, 1);
|
|
assert.ok(loserTokens.includes(changed[0].id));
|
|
assert.equal(done.techTokens![changed[0].id].owner, f.richese);
|
|
}
|
|
assert.equal(done.discard.filter(c => c.id === f.green).length, 1);
|
|
assert.deepEqual(normalizeAutomaticGame(reload(done)), done);
|
|
custody(done);
|
|
});
|
|
}
|
|
|
|
void test('four minimal policies resolve actual nested marker plans into physical revelation and a completed casualty/skill consumer', (t) => {
|
|
// The deal is pinned: an unseeded Lasgun-shield pair explodes, destroys both
|
|
// armies and legitimately leaves no winner, which would pre-empt the casualty
|
|
// and skill consumer this test exists to exercise.
|
|
pinDeterministicRandom(t);
|
|
const f = createDiscoveryNativeE2SkillsFixture({ advanced: true, tech: true, strongholds: true });
|
|
const battle = prepareDiscoveryNativeE2SkillsMarkerBattle(f, { marker: 5, reserveCap: 3 });
|
|
for (const difficulty of DIFFICULTIES) {
|
|
let game = reload(battle.game);
|
|
for (let step = 0; !game.battle?.revealed && step < 100; step++) {
|
|
let advanced = false;
|
|
for (const seat of game.players) {
|
|
const view = viewGame(game, seat.id);
|
|
view.players.find(p => p.id === seat.id)!.bot = difficulty;
|
|
const action = botActions(view)[0];
|
|
if (!action) continue;
|
|
game = applyAction(game, seat.id, action);
|
|
advanced = true;
|
|
break;
|
|
}
|
|
assert.ok(advanced, `${difficulty} needs an actual owned card, response or plan continuation.`);
|
|
}
|
|
assert.ok(game.battle?.revealed);
|
|
assert.equal(player(game, f.richese).noField!.deployed, null);
|
|
assert.equal(player(game, f.richese).forces[battle.key], 3);
|
|
assert.equal(player(game, f.richese).reserves, 0);
|
|
const done = finishDiscoveryNativeE2SkillsBattle(game);
|
|
assert.ok(done.lastBattleContext?.winner === f.richese || done.lastBattleContext?.winner === f.guild,
|
|
`The non-exploding marker battle must resolve to a real winner, not ${done.lastBattleContext?.result}.`);
|
|
assert.equal(done.battle, null);
|
|
assert.equal(player(done, f.richese).noField!.deployed, null);
|
|
assert.equal(player(done, f.richese).noField!.lastShipped, battle.token);
|
|
assert.equal(player(done, f.richese).tanks + (player(done, f.richese).forces[battle.key] ?? 0), 3);
|
|
assert.deepEqual(player(done, f.richese).noField!.tokens, player(battle.game, f.richese).noField!.tokens);
|
|
custody(done);
|
|
}
|
|
});
|