130 lines
7.5 KiB
TypeScript
130 lines
7.5 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { viewGame, type Action, type Game } from '../game/engine';
|
|
import { windowCardOpportunity } from '../game/window-card-play';
|
|
import { botActions } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import { actTruthRevival as act, createTruthRevivalFixture, declareTruthRevival,
|
|
settleTruthRevival as settle, truthRevivalInventory as inventory,
|
|
truthRevivalPlayer as player } from './fixture-truth-revival';
|
|
|
|
/** Native Advanced Revival after Emperor shipped and lost three forces in the
|
|
* original Storm; physical Truthtrance and Karama come from the real deal. */
|
|
function fixture() {
|
|
const f = createTruthRevivalFixture({ advanced: true, targetKarama: true });
|
|
assert.ok(f.auctionKarama);
|
|
const source = JSON.stringify(['karamaEmperorSpecial', f.game.turn, f.target]);
|
|
assert.equal(windowCardOpportunity(f.game, 'karamaEmperorSpecial')?.source, source);
|
|
return { ...f, source, karamaCard: f.auctionKarama };
|
|
}
|
|
function question(f: ReturnType<typeof fixture>, target = f.target) {
|
|
return { kind: 'turnActions', target, turn: f.game.turn, mixed: { kind: 'action', action: {
|
|
kind: 'windowCardPlay', effect: 'karamaEmperorSpecial', source: f.source,
|
|
} } };
|
|
}
|
|
function ask(f: ReturnType<typeof fixture>, state = f.game) {
|
|
const declared = declareTruthRevival(f, state);
|
|
return act(declared.game, f.asker, { type: 'truthAsk', question: question(f) });
|
|
}
|
|
function reject(game: Game, actor: string, action: Action) {
|
|
const before = structuredClone(game);
|
|
assert.throws(() => act(game, actor, action));
|
|
assert.deepEqual(game, before);
|
|
}
|
|
|
|
void test('Advanced Emperor special Karama Yes uses a physical card for a real free force return', () => {
|
|
const f = fixture(), asked = ask(f);
|
|
assert.ok(viewGame(asked, f.target).truthTurnActionAnswers?.includes('yes'));
|
|
assert.equal(viewGame(asked, f.opponent).truthTurnActionAnswers, null);
|
|
let game = act(asked, f.target, { type: 'truthAnswer', answer: 'yes' });
|
|
const guide = viewGame(game, f.target).turnActionGuide; assert.ok(guide);
|
|
assert.equal(guide.action.type, 'card');
|
|
assert.equal(guide.action.mode, 'special');
|
|
assert.equal(guide.action.card, f.karamaCard);
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const bot = viewGame(game, f.target);
|
|
bot.players.find(seat => seat.id === f.target)!.bot = difficulty;
|
|
assert.deepEqual(botActions(bot)[0], guide.action);
|
|
}
|
|
const before = structuredClone(game), priorTanks = player(game, f.target).tanks;
|
|
const priorReserves = player(game, f.target).reserves, priorSpice = player(game, f.target).spice;
|
|
// A voluntary ordinary return using every tank counter cannot eliminate the
|
|
// only native special-return witness for the accepted Yes.
|
|
reject(game, f.target, { type: 'revive', amount: 3, elite: 1 });
|
|
game = act(game, f.target, guide.action);
|
|
assert.deepEqual(before.turnActionPromises?.[0].windowCards?.karamaEmperorSpecial?.played, false);
|
|
assert.equal(player(game, f.target).specialKaramaUsed, true);
|
|
assert.equal(game.turnActionPromises!.find(row => row.player === f.target)!.windowCards!.karamaEmperorSpecial!.played, true);
|
|
assert.equal(game.turnActionPromises!.find(row => row.player === f.target)!.fulfilled, true);
|
|
const returned = priorTanks - player(game, f.target).tanks;
|
|
assert.ok(returned >= 1 && returned <= 3);
|
|
assert.equal(player(game, f.target).reserves, priorReserves + returned);
|
|
assert.equal(player(game, f.target).spice, priorSpice);
|
|
assert.equal(game.discard.filter(card => card.id === f.karamaCard).length, 1);
|
|
assert.equal(viewGame(game, f.asker).players.find(seat => seat.id === f.target)!.hand, undefined);
|
|
assert.deepEqual(viewGame(game, f.asker).turnActionPromises, []);
|
|
game = settle(game);
|
|
assert.deepEqual(inventory(game), f.originalCards);
|
|
});
|
|
|
|
void test('Advanced Emperor special Karama No blocks special use without spending the card', () => {
|
|
const f = fixture(), asked = ask(f);
|
|
assert.ok(viewGame(asked, f.target).truthTurnActionAnswers?.includes('no'));
|
|
const game = act(asked, f.target, { type: 'truthAnswer', answer: 'no' });
|
|
reject(game, f.target, { type: 'card', card: f.karamaCard, mode: 'special', amount: 1, elite: 0 });
|
|
assert.ok(player(game, f.target).hand.some(card => card.id === f.karamaCard));
|
|
assert.notEqual(player(game, f.target).specialKaramaUsed, true);
|
|
assert.deepEqual(inventory(game), f.originalCards);
|
|
});
|
|
|
|
void test('Advanced Emperor can use special Karama to revive a dead leader after Yes', () => {
|
|
const f = fixture(), staged = structuredClone(f.game), emperor = player(staged, f.target);
|
|
// This separate branch stages one existing Emperor leader's death, without
|
|
// creating a leader or changing any card or force custody.
|
|
const leader = emperor.leaders[0]; assert.ok(leader && !leader.dead);
|
|
leader.dead = true; leader.deaths = 1; delete leader.usedAt;
|
|
const asked = ask(f, staged);
|
|
assert.ok(viewGame(asked, f.target).truthTurnActionAnswers?.includes('yes'));
|
|
let game = act(asked, f.target, { type: 'truthAnswer', answer: 'yes' });
|
|
game = act(game, f.target, { type: 'card', card: f.karamaCard, mode: 'special', leader: leader.id });
|
|
assert.equal(player(game, f.target).leaders.find(row => row.id === leader.id)!.dead, false);
|
|
assert.equal(game.turnActionPromises!.find(row => row.player === f.target)!.windowCards!.karamaEmperorSpecial!.played, true);
|
|
assert.equal(game.turnActionPromises!.find(row => row.player === f.target)!.fulfilled, true);
|
|
game = settle(game);
|
|
assert.deepEqual(inventory(game), f.originalCards);
|
|
});
|
|
|
|
void test('the Emperor special source rejects other respondents and forged turns', () => {
|
|
const f = fixture(), game = declareTruthRevival(f).game;
|
|
reject(game, f.asker, { type: 'truthAsk', question: question(f, f.opponent) });
|
|
const forged = question(f);
|
|
forged.mixed.action.source = JSON.stringify(['karamaEmperorSpecial', game.turn + 1, f.target]);
|
|
reject(game, f.asker, { type: 'truthAsk', question: forged });
|
|
});
|
|
|
|
void test('Emperor special guide uses its own promise event after an earlier unrelated whole-turn answer', () => {
|
|
const f = fixture();
|
|
// Both original Truthtrances were dealt to the asker by the native fixture.
|
|
let game = declareTruthRevival(f, f.game, { copies: 2 }).game;
|
|
game = act(game, f.asker, { type: 'truthAsk', question: {
|
|
kind: 'turnActions', target: f.target, turn: game.turn,
|
|
mixed: { kind: 'action', action: { kind: 'cardPlay', effect: 'hajr' } },
|
|
} });
|
|
assert.ok(viewGame(game, f.target).truthTurnActionAnswers?.includes('no'));
|
|
game = act(game, f.target, { type: 'truthAnswer', answer: 'no' });
|
|
assert.equal(game.truthtrance?.stage, 'ask');
|
|
game = act(game, f.asker, { type: 'truthAsk', question: question(f) });
|
|
assert.ok(viewGame(game, f.target).truthTurnActionAnswers?.includes('yes'));
|
|
game = act(game, f.target, { type: 'truthAnswer', answer: 'yes' });
|
|
const rows = game.turnActionPromises!.filter(row => row.player === f.target && !row.fulfilled && !row.released);
|
|
assert.equal(rows.length, 2);
|
|
const special = rows.find(row => row.windowCards?.karamaEmperorSpecial); assert.ok(special);
|
|
const guide = viewGame(game, f.target).turnActionGuide; assert.ok(guide);
|
|
assert.equal(guide.event, special.event);
|
|
assert.equal(guide.action.turnActionPromiseEvent, special.event);
|
|
assert.equal(guide.action.mode, 'special');
|
|
game = act(game, f.target, guide.action);
|
|
assert.ok(game.turnActionPromises!.find(row => row.event === special.event)?.fulfilled);
|
|
assert.deepEqual(inventory(settle(game)), f.originalCards);
|
|
});
|