334 lines
21 KiB
TypeScript
334 lines
21 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { applyAction, createGame, joinGame, newPlayer, viewGame, type Game } from '../game/engine';
|
|
import { botActions } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import { FACTIONS } from '../game/catalog';
|
|
import { treacheryDeck, isAuditorLeader } from '../game/cards';
|
|
import { richeseCards } from '../game/richese-cards';
|
|
import { LEADER_SKILL_CARDS } from '../game/leader-skill-cards';
|
|
import { ownedTech } from '../game/tech-tokens';
|
|
import { quoteSpiceCollection } from '../game/board-resolution-quote';
|
|
import {
|
|
assertNativeSkillsStrongholdCustody as custody, createNativeSkillsStrongholdFixture as fixture,
|
|
finishNativeSkillsStrongholdTurn, nativeSkillsStrongholdControllers,
|
|
nativeSkillsStrongholdPlayer as player, nextNativeSkillsStrongholdStep,
|
|
quoteNativeSkillsStrongholdBattle, revealNativeSkillsStrongholdBattle as reveal,
|
|
settleNativeSkillsStrongholdBattle as settle, type NativeSkillsStrongholdFixture,
|
|
} from './fixture-native-skills-stronghold';
|
|
|
|
/** Native Collection is an independent later receipt, not a battle subsidy or bounty. */
|
|
function collectionIncome(game: Game, id: string): number {
|
|
if (game.phase !== 7 || game.phaseOpening) return 0;
|
|
return quoteSpiceCollection({ ...game, players: game.players.map(p => ({ ...p, spice: 0 })) }).receipts
|
|
.filter(r => r.player === id).reduce((sum, r) => sum + r.strongholds + r.collected, 0);
|
|
}
|
|
function assertBattleWallets(before: Game, after: Game): void {
|
|
const quote = quoteNativeSkillsStrongholdBattle(before);
|
|
for (const p of before.players) {
|
|
const paid = quote.payments.find(r => r.player === p.id)?.ownPayment ?? 0;
|
|
const stronghold = quote.strongholdIncome.filter(r => r.player === p.id).reduce((sum, r) => sum + r.amount, 0);
|
|
const bounty = quote.bounty?.player === p.id ? quote.bounty.amount : 0;
|
|
const choam = quote.choamIncome?.owner === p.id ? quote.choamIncome.amount : 0;
|
|
assert.equal(player(after, p.id).spice, p.spice - paid + stronghold + bounty + choam + collectionIncome(after, p.id), p.faction);
|
|
}
|
|
}
|
|
function afterCopy(f: NativeSkillsStrongholdFixture): Game {
|
|
assert.ok(f.copyAction);
|
|
let game = applyAction(f.game, f.owner, f.copyAction);
|
|
for (let i = 0; (game.decision || game.response || game.battle?.preparation || game.battle?.preLeader?.closed === false) && i < 100; i++) {
|
|
const next = nextNativeSkillsStrongholdStep(game); assert.ok(next);
|
|
game = applyAction(game, next.actor, next.action);
|
|
}
|
|
return game;
|
|
}
|
|
|
|
void test('original Advanced native offers, wallets, decks/cache and physical 20/7/5/3 survive genuine Stronghold and optional Tech setup', () => {
|
|
for (const kind of ['ix-copy-suk', 'tleilaxu-tabr', 'richese-marker'] as const) {
|
|
for (const optionalTech of [false, true]) {
|
|
const f = fixture({ kind, optionalTech });
|
|
const skills = f.offered.leaderSkills!;
|
|
assert.deepEqual([...skills.deck, ...Object.values(skills.offers).flatMap(o => o.cards), ...skills.assignments.map(a => a.skill)].sort(),
|
|
LEADER_SKILL_CARDS.map(c => c.id).sort());
|
|
for (const p of f.afterSetup.players) assert.equal(p.spice, FACTIONS.find(c => c.id === p.faction)!.spice, p.faction);
|
|
const physical = [...f.afterSetup.deck, ...f.afterSetup.discard, ...f.afterSetup.players.flatMap(p => p.hand), ...(f.afterSetup.richeseCache ?? [])];
|
|
const canonical = [...treacheryDeck(f.afterSetup.expansions), ...(kind === 'richese-marker' ? richeseCards() : [])];
|
|
assert.deepEqual(physical.map(c => c.id).sort(), canonical.map(c => c.id).sort());
|
|
assert.equal(nativeSkillsStrongholdControllers(f.beforeFirstMentat)[f.territory], f.owner);
|
|
assert.equal(f.beforeFirstMentat.strongholdCards!.owners[f.territory], null);
|
|
const claimed = applyAction(f.beforeFirstMentat, f.firstMentatStep.actor, f.firstMentatStep.action);
|
|
assert.equal(claimed.strongholdCards!.owners[f.territory], f.owner);
|
|
assert.equal(claimed.turn, 2);
|
|
if (optionalTech) {
|
|
const seats = new Set(f.afterFirstMentat.players.map(p => p.id));
|
|
assert.ok(Object.values(f.afterFirstMentat.techTokens!).every(token => token.owner && seats.has(token.owner)));
|
|
if (kind !== 'richese-marker') {
|
|
const native = f.afterSetup.players.find(p => p.faction === (kind === 'ix-copy-suk' ? 'ixians' : 'tleilaxu'))!;
|
|
assert.ok(ownedTech(f.afterFirstMentat.techTokens, native.id).includes(kind === 'ix-copy-suk' ? 'heighliners' : 'axlotl'));
|
|
}
|
|
}
|
|
custody(f.afterSetup); custody(f.afterFirstMentat); custody(f.game);
|
|
}
|
|
}
|
|
});
|
|
|
|
void test('authenticated original lobby keeps human IDs and native decks while actually reaching HMS copy and typed trained-disc Suk', () => {
|
|
const initial = createGame('NATIVEHUMAN', newPlayer('human-ix', 'Human Ix', 'ixians'), true, ['ix', 'choam']);
|
|
joinGame(initial, newPlayer('human-emperor', 'Human Emperor', 'emperor'));
|
|
joinGame(initial, newPlayer('human-choam', 'Human CHOAM', 'choam'));
|
|
joinGame(initial, newPlayer('human-fremen', 'Human Fremen', 'fremen'));
|
|
const before = structuredClone(initial);
|
|
const f = fixture({ initial, kind: 'ix-copy-suk', optionalTech: 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');
|
|
assert.equal(f.game.decision?.kind, 'strongholdCopy');
|
|
assert.deepEqual(f.game.decision?.kind === 'strongholdCopy' ? [...f.game.decision.choices].sort() : [], ['arrakeen', 'sietch_tabr']);
|
|
const pending = structuredClone(f.game);
|
|
assert.throws(() => applyAction(f.game, f.owner, f.plans[0].action));
|
|
assert.deepEqual(f.game, pending, 'No hidden plan can precede the public HMS copy declaration');
|
|
const ready = afterCopy(f);
|
|
assert.equal(viewGame(ready, f.opponent).battle!.strongholdEffects[f.owner], 'arrakeen');
|
|
const revealed = reveal(f), quote = quoteNativeSkillsStrongholdBattle(revealed);
|
|
assert.equal(quote.payments.find(p => p.player === f.owner)!.bankSupport, 1);
|
|
assert.equal(quote.payments.find(p => p.player === f.owner)!.ownPayment, 0);
|
|
const rescue = settle(revealed, 'suk');
|
|
assert.equal(rescue.decision?.kind, 'sukRescue');
|
|
if (rescue.decision?.kind !== 'sukRescue') throw Error('Native typed casualties must reach Suk');
|
|
assert.deepEqual(rescue.pendingSukRescue!.losses, { normal: 4, elite: 2, paidNormal: 0, paidElite: 1 });
|
|
const choice = rescue.decision.options.findIndex(o => o.normal === 2 && o.elite === 1 && o.kept?.kind === 'normal');
|
|
assert.ok(choice >= 0);
|
|
const resolved = settle(applyAction(rescue, f.owner, { type: 'decision', event: rescue.decision.event, choice }));
|
|
assert.equal(resolved.lastBattleContext!.winner, f.owner);
|
|
assert.equal(player(resolved, f.owner).forces[f.location], 3);
|
|
assert.equal(player(resolved, f.owner).elites!.forces[f.location] ?? 0, 0);
|
|
assert.equal(player(resolved, f.owner).elites!.reserves, 6);
|
|
assert.equal(player(resolved, f.owner).elites!.tanks, 1);
|
|
assert.equal(player(resolved, f.owner).tanks, 3);
|
|
assert.equal(player(resolved, f.opponent).tanks, 8);
|
|
assertBattleWallets(revealed, resolved); custody(resolved);
|
|
});
|
|
|
|
void test('actual Arrakeen subsidy funds trained Suk without suppressing CHOAM original declared-support income', () => {
|
|
const f = fixture({ kind: 'choam-support', optionalTech: true });
|
|
const revealed = reveal(f), quote = quoteNativeSkillsStrongholdBattle(revealed);
|
|
assert.equal(quote.payments.find(r => r.player === f.owner)!.ownPayment, 2);
|
|
assert.equal(quote.payments.find(r => r.player === f.owner)!.bankSupport, 2);
|
|
assert.deepEqual(quote.choamIncome, { owner: f.choam, amount: 2 });
|
|
const resolved = settle(revealed);
|
|
assert.equal(player(resolved, f.owner).forces[f.location], 5);
|
|
assert.equal(player(resolved, f.owner).tanks, 1);
|
|
assert.equal(player(resolved, f.choam!).spice, player(revealed, f.choam!).spice + 2);
|
|
assertBattleWallets(revealed, resolved); custody(resolved);
|
|
});
|
|
|
|
void test('native CHOAM Auditor is excluded from training and cannot borrow a hidden trained Suk rescue', () => {
|
|
const f = fixture({ kind: 'choam-auditor' });
|
|
const eligible = viewGame(f.offered, f.owner).leaderSkills!.eligibleLeaders;
|
|
const auditor = player(f.afterSetup, f.owner).leaders.find(isAuditorLeader)!;
|
|
assert.ok(auditor && eligible.every(l => l.id !== auditor.id));
|
|
assert.ok(f.afterSetup.leaderSkills!.assignments.every(a => a.leader !== auditor.id));
|
|
const revealed = reveal(f), quote = quoteNativeSkillsStrongholdBattle(revealed);
|
|
assert.equal(quote.auditor!.owner, f.owner);
|
|
assert.equal(quote.auditor!.survived, true);
|
|
const resolved = settle(revealed);
|
|
assert.equal(resolved.lastBattleContext!.winner, f.owner);
|
|
assert.equal(player(resolved, f.owner).forces[f.location], 4);
|
|
assert.equal(player(resolved, f.owner).tanks, 4, 'Unskilled Auditor has no trained-disc Suk rescue');
|
|
assertBattleWallets(revealed, resolved); custody(resolved);
|
|
});
|
|
|
|
void test('Tabr/Tuek original winner keeps spice/cards and optional Tech before Face Dance; matching-disc death returns Suk once without a second bounty', () => {
|
|
for (const kind of ['tleilaxu-tabr', 'tleilaxu-tuek'] as const) {
|
|
for (const optionalTech of [false, true]) {
|
|
const f = fixture({ kind, optionalTech });
|
|
const revealed = reveal(f), quote = quoteNativeSkillsStrongholdBattle(revealed);
|
|
assert.deepEqual(quote.strongholdIncome, [{ player: f.owner, amount: 2 }]);
|
|
const suk = settle(revealed, 'suk');
|
|
assert.equal(suk.decision?.kind, 'sukRescue');
|
|
const cards = settle(suk, 'cards');
|
|
assert.equal(cards.decision?.kind, 'battleCards');
|
|
assert.equal(cards.decision?.player, f.owner);
|
|
const usedDefense = f.plans[0].action.defense as string;
|
|
const cleaned = applyAction(cards, f.owner, { type: 'decision', discard: [usedDefense] });
|
|
const face = settle(cleaned, 'faceDance');
|
|
assert.equal(face.decision?.kind, 'faceDance');
|
|
assert.ok(face.discard.some(c => c.id === usedDefense), 'Original winner card choice finishes before Face Dance');
|
|
assert.equal(player(face, f.owner).forces[f.location], 5, 'Original Suk rescue completes first');
|
|
assert.equal(player(face, f.owner).tanks, 1);
|
|
assert.equal(player(face, f.owner).spice, player(revealed, f.owner).spice - 4 + quote.bounty!.amount + 2);
|
|
if (optionalTech) assert.ok(ownedTech(face.techTokens, f.owner).includes('axlotl'));
|
|
const cardIds = player(face, f.owner).hand.map(c => c.id).sort();
|
|
const reserves = player(face, f.owner).reserves;
|
|
const dancerReserves = player(face, f.tleilaxu!).reserves;
|
|
const danced = applyAction(face, f.tleilaxu!, { type: 'decision', reveal: true,
|
|
sources: { reserves: 1, [f.boardSource]: 2 }, sector: Number(f.location.split(':')[1]) });
|
|
assert.equal(player(danced, f.owner).reserves, reserves + 5);
|
|
assert.equal(player(danced, f.owner).forces[f.location] ?? 0, 0);
|
|
assert.equal(player(danced, f.tleilaxu!).forces[f.location], 3);
|
|
assert.equal(player(danced, f.tleilaxu!).forces[f.boardSource], 1);
|
|
assert.equal(player(danced, f.tleilaxu!).reserves, dancerReserves - 1);
|
|
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 === 'suk-graduate').length, 1);
|
|
assert.deepEqual(player(danced, f.owner).hand.map(c => c.id).sort(), cardIds);
|
|
for (const p of face.players)
|
|
assert.equal(player(danced, p.id).spice, p.spice + collectionIncome(danced, p.id),
|
|
`${p.faction}: Face Dance awards no additional leader bounty or confiscation`);
|
|
if (optionalTech) assert.ok(ownedTech(danced.techTokens, f.owner).includes('axlotl'));
|
|
assert.equal(nativeSkillsStrongholdControllers(danced)[f.territory], f.tleilaxu);
|
|
assert.equal(danced.strongholdCards!.owners[f.territory], f.owner, 'Board replacement does not immediately transfer retained Stronghold Card');
|
|
const ended = finishNativeSkillsStrongholdTurn(danced);
|
|
assert.equal(ended.strongholdCards!.owners[f.territory], f.tleilaxu);
|
|
assert.equal(ended.leaderSkills!.deck.filter(s => s === 'suk-graduate').length, 1);
|
|
custody(danced); custody(ended);
|
|
}
|
|
}
|
|
});
|
|
|
|
void test('mixed ordinary-force/No-Field battle retains trained Suk and its Stronghold Card through real capped losses', () => {
|
|
const f = fixture({ kind: 'richese-marker' });
|
|
let game = structuredClone(f.beforeBattle);
|
|
const richese = player(game, f.owner);
|
|
// Explicit conserved alternative board position: one existing Polar Sink
|
|
// counter accompanies the marker, not a shipment or played-turn claim.
|
|
richese.forces[f.boardSource]--;
|
|
richese.forces[f.location] = 1;
|
|
const actor = game.active!;
|
|
game = applyAction(game, actor, { type: 'chooseBattle', territory: f.territory,
|
|
target: actor === f.owner ? f.opponent : f.owner });
|
|
for (let i = 0; (game.decision || game.response || game.battle?.preparation || game.battle?.preLeader?.closed === false) && i < 100; i++) {
|
|
const next = nextNativeSkillsStrongholdStep(game); assert.ok(next);
|
|
game = applyAction(game, next.actor, next.action);
|
|
}
|
|
assert.equal(viewGame(game, f.owner).battle!.mixedNoField, true);
|
|
assert.equal(viewGame(game, f.owner).battle!.ownForces!.normal, 4);
|
|
assert.equal(viewGame(game, f.opponent).battle!.mixedNoField, undefined);
|
|
const before = structuredClone(game);
|
|
assert.throws(() => applyAction(game, f.owner, { ...f.plans[0].action, dial: 5 }));
|
|
assert.deepEqual(game, before);
|
|
game = applyAction(game, f.owner, f.plans[0].action);
|
|
assert.equal(game.battle!.revealed, false);
|
|
assert.ok(player(game, f.owner).noField!.deployed);
|
|
assert.equal(player(game, f.owner).forces[f.location], 1);
|
|
assert.equal(player(game, f.owner).reserves, 3);
|
|
game = applyAction(game, f.opponent, f.plans[1].action);
|
|
assert.equal(game.battle!.revealed, true);
|
|
assert.equal(player(game, f.owner).noField!.deployed, null);
|
|
assert.equal(player(game, f.owner).forces[f.location], 4);
|
|
assert.equal(player(game, f.owner).reserves, 0);
|
|
const revealed = game;
|
|
assert.equal(quoteNativeSkillsStrongholdBattle(revealed).payments.find(p => p.player === f.owner)!.bankSupport, 2);
|
|
const resolved = settle(revealed);
|
|
assert.equal(resolved.lastBattleContext!.winner, f.owner);
|
|
assert.equal(player(resolved, f.owner).forces[f.location], 2);
|
|
assert.equal(player(resolved, f.owner).reserves, 2);
|
|
assert.equal(player(resolved, f.owner).tanks, 0);
|
|
assert.equal(player(resolved, f.opponent).tanks, 6);
|
|
assert.equal(resolved.strongholdCards!.owners[f.territory], f.owner);
|
|
assertBattleWallets(revealed, resolved); custody(resolved);
|
|
});
|
|
|
|
void test('reserve-capped marker-only Richese materializes three after both plans, earns retained Arrakeen subsidy and rescues only physical casualties', () => {
|
|
const f = fixture({ kind: 'richese-marker', optionalTech: true });
|
|
assert.equal(player(f.shipment!.after, f.owner).spice, player(f.shipment!.before, f.owner).spice - 1);
|
|
const own = viewGame(f.game, f.owner).battle!;
|
|
assert.equal(own.ownForces!.normal, 3);
|
|
const before = structuredClone(f.game);
|
|
assert.throws(() => applyAction(f.game, f.owner, { ...f.plans[0].action, dial: 4 }));
|
|
assert.deepEqual(f.game, before);
|
|
const sealed = applyAction(f.game, f.owner, f.plans[0].action);
|
|
assert.ok(player(sealed, f.owner).noField!.deployed);
|
|
assert.equal(player(sealed, f.owner).forces[f.location] ?? 0, 0);
|
|
const revealed = reveal(f), quote = quoteNativeSkillsStrongholdBattle(revealed);
|
|
assert.equal(player(revealed, f.owner).forces[f.location], 3);
|
|
assert.equal(quote.payments.find(p => p.player === f.owner)!.ownPayment, 1);
|
|
assert.equal(quote.payments.find(p => p.player === f.owner)!.bankSupport, 2);
|
|
const resolved = settle(revealed);
|
|
assert.equal(player(resolved, f.owner).forces[f.location], 1);
|
|
assert.equal(player(resolved, f.owner).reserves, 2);
|
|
assert.equal(player(resolved, f.owner).tanks, 0);
|
|
assert.equal(player(resolved, f.opponent).tanks, 6);
|
|
assertBattleWallets(revealed, resolved); custody(resolved);
|
|
});
|
|
|
|
void test('native cache-acquired Stone uses retained Habbanya in both modes with all skills hidden and untrained battle discs', () => {
|
|
for (const mode of ['kill', 'ignore'] as const) {
|
|
const f = fixture({ kind: 'richese-stone' });
|
|
assert.ok(f.acquisition!.before.richeseCache!.some(c => c.id === 'richese-stone-burner'));
|
|
assert.ok(player(f.acquisition!.after, f.owner).hand.some(c => c.id === 'richese-stone-burner'));
|
|
assert.equal(f.acquisition!.after.richeseCache!.some(c => c.id === 'richese-stone-burner'), false);
|
|
for (const p of f.acquisition!.before.players) assert.equal(player(f.acquisition!.after, p.id).spice, p.spice);
|
|
const offered = reveal(f);
|
|
assert.equal(offered.decision?.kind, 'stoneBurner');
|
|
const chosen = applyAction(offered, f.owner, { type: 'decision', event: offered.battle!.event, mode });
|
|
const quote = quoteNativeSkillsStrongholdBattle(chosen), resolved = settle(chosen);
|
|
assert.equal(quote.winner, f.owner);
|
|
assert.deepEqual(quote.stone!.attacker, [6]);
|
|
assert.deepEqual(quote.stone!.defender, [6]);
|
|
assert.equal(player(resolved, f.owner).forces[f.location], 6);
|
|
assert.equal(player(resolved, f.owner).tanks, 2);
|
|
for (const plan of f.plans) assert.equal(player(resolved, plan.actor).leaders.find(l => l.id === plan.action.leader)!.dead, mode === 'kill');
|
|
assert.ok(player(resolved, f.owner).hand.some(c => c.id === 'richese-stone-burner'));
|
|
assertBattleWallets(chosen, resolved); custody(resolved);
|
|
}
|
|
});
|
|
|
|
void test('four existing policies finish changed HMS/Suk and capped-marker windows with physical and economic outcomes', () => {
|
|
const ix = fixture({ kind: 'ix-copy-suk' });
|
|
const marker = fixture({ kind: 'richese-marker' });
|
|
const stone = fixture({ kind: 'richese-stone' });
|
|
const stoneChoice = reveal(stone);
|
|
const ixRescue = settle(reveal(ix), 'suk');
|
|
const markerRescue = settle(reveal(marker), 'suk');
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const copyView = viewGame(ix.game, ix.owner); copyView.players.find(p => p.id === ix.owner)!.bot = difficulty;
|
|
const copy = botActions(copyView)[0];
|
|
const declared = applyAction(ix.game, ix.owner, copy);
|
|
assert.ok(['arrakeen', 'sietch_tabr'].includes(declared.battle!.strongholdCopy!));
|
|
const copiedBattle = reveal({ ...ix, copyAction: copy });
|
|
const copiedQuote = quoteNativeSkillsStrongholdBattle(copiedBattle);
|
|
assert.equal(copiedQuote.payments.find(p => p.player === ix.owner)!.ownPayment,
|
|
declared.battle!.strongholdCopy === 'arrakeen' ? 0 : 1);
|
|
const copiedResult = settle(copiedBattle);
|
|
assert.equal(player(copiedResult, ix.owner).forces[ix.location], 3);
|
|
assert.equal(player(copiedResult, ix.owner).tanks, 3);
|
|
assertBattleWallets(copiedBattle, copiedResult);
|
|
let markerPlans = structuredClone(marker.game);
|
|
for (const actor of [marker.owner, marker.opponent]) {
|
|
const view = viewGame(markerPlans, actor); view.players.find(p => p.id === actor)!.bot = difficulty;
|
|
const plan = botActions(view).find(a => a.type === 'battlePlan'); assert.ok(plan);
|
|
markerPlans = applyAction(markerPlans, actor, plan);
|
|
for (let n = 0; !markerPlans.battle?.revealed && (markerPlans.response || markerPlans.decision) && n < 100; n++) {
|
|
const next = nextNativeSkillsStrongholdStep(markerPlans); assert.ok(next);
|
|
markerPlans = applyAction(markerPlans, next.actor, next.action);
|
|
}
|
|
}
|
|
assert.equal(player(markerPlans, marker.owner).forces[marker.location], 3);
|
|
const markerResult = settle(markerPlans);
|
|
assertBattleWallets(markerPlans, markerResult); custody(markerResult);
|
|
const stoneView = viewGame(stoneChoice, stone.owner); stoneView.players.find(p => p.id === stone.owner)!.bot = difficulty;
|
|
const mode = botActions(stoneView)[0];
|
|
const chosenStone = applyAction(stoneChoice, stone.owner, mode), stoneResult = settle(chosenStone);
|
|
assert.equal(stoneResult.lastBattleContext!.winner, stone.owner);
|
|
assert.equal(player(stoneResult, stone.owner).forces[stone.location], 6);
|
|
for (const plan of stone.plans)
|
|
assert.equal(player(stoneResult, plan.actor).leaders.find(l => l.id === plan.action.leader)!.dead, mode.mode === 'kill');
|
|
assertBattleWallets(chosenStone, stoneResult); custody(stoneResult);
|
|
for (const [f, pending] of [[ix, ixRescue], [marker, markerRescue]] as const) {
|
|
const view = viewGame(pending, f.owner); view.players.find(p => p.id === f.owner)!.bot = difficulty;
|
|
const action = botActions(view)[0];
|
|
const rescued = settle(applyAction(pending, f.owner, action));
|
|
assert.equal(rescued.lastBattleContext!.winner, f.owner);
|
|
assert.equal(player(rescued, f.owner).tanks, f.kind === 'richese-marker' ? 0 : 3,
|
|
`${difficulty}/${f.kind}: three real casualties saved, no unpaid cyborg manufactured`);
|
|
assert.equal(player(rescued, f.opponent).tanks, f.kind === 'richese-marker' ? 6 : 8);
|
|
if (f.kind === 'richese-marker') {
|
|
assert.equal(player(rescued, f.owner).forces[f.location], 1);
|
|
assert.equal(player(rescued, f.owner).reserves, 2);
|
|
} else assert.equal(player(rescued, f.owner).forces[f.location], 3);
|
|
custody(rescued);
|
|
}
|
|
}
|
|
});
|