91 lines
5.2 KiB
TypeScript
91 lines
5.2 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { applyAction, viewGame, type Game } from '../game/engine';
|
|
import { botActions } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
|
|
import { semutaSkillsInventory } from './fixture-semuta-skills';
|
|
import { finishOriginalTruthQuestions, finishTruthOrder } from './fixture-sapho-truth-order';
|
|
import { selectedDiscoverySkillsStart, advanceDiscoverySkillsBlow } from './selected-discovery-skills-fixture';
|
|
|
|
const SAPHO = 'richese-juice-of-sapho';
|
|
const seat = (game: Game, id: string) => {
|
|
const player = game.players.find(row => row.id === id); assert.ok(player); return player;
|
|
};
|
|
|
|
/** Real public mixed-deck setup, training and Discovery blow. Reorder only
|
|
* unopened Spice Cards and conserve every physical Treachery Card while
|
|
* staging the two simultaneous question holders and Sapho/Semuta custody. */
|
|
function fixture(advanced: boolean) {
|
|
const started = selectedDiscoverySkillsStart({advanced,
|
|
factions:['richese', 'ecaz', 'atreides', 'emperor']});
|
|
assert.equal(started.setup.semutaPreview, true);
|
|
assert.ok(started.game.leaderSkills && started.game.discoveries);
|
|
let game = advanceDiscoverySkillsBlow(started.game, started.actions);
|
|
const inventory = semutaSkillsInventory(game);
|
|
for (const player of game.players) game.deck.push(...player.hand.splice(0));
|
|
const give = (id: string, identity: string) => {
|
|
const zones = [game.deck, game.discard, game.richeseCache!, ...game.players.map(row => row.hand)];
|
|
const source = zones.find(zone => zone.some(card => card.id === identity || card.effect === identity));
|
|
assert.ok(source, `Missing original ${identity}`);
|
|
const [card] = source.splice(source.findIndex(row => row.id === identity || row.effect === identity), 1);
|
|
seat(game, id).hand.push(card);
|
|
return card.id;
|
|
};
|
|
const ordered = game.order.filter(id => id === 'atreides' || id === 'emperor');
|
|
assert.equal(ordered.length, 2);
|
|
const [other, holder] = ordered;
|
|
give('richese', SEMUTA_DRUG_ID);
|
|
give(holder, SAPHO);
|
|
const truth = give(holder, 'truthtrance'), otherTruth = give(other, 'truthtrance');
|
|
assert.deepEqual(semutaSkillsInventory(game), inventory);
|
|
const parent = {turn:game.turn, phase:game.phase, active:game.active,
|
|
order:[...game.order], movementRemaining:structuredClone(game.movementRemaining)};
|
|
for (const [id, card] of [[holder, truth], [other, otherTruth]])
|
|
game = applyAction(game, id, {type:'card', card});
|
|
while (game.truthtrance?.stage === 'priority') {
|
|
const actor = game.players.find(row => !game.truthtrance!.passed.includes(row.id)); assert.ok(actor);
|
|
game = applyAction(game, actor.id, {type:'truthPass'});
|
|
}
|
|
assert.equal(game.truthtrance?.stage, 'order');
|
|
return {game, holder, parent, inventory};
|
|
}
|
|
|
|
for (const advanced of [false, true]) void test(
|
|
`${advanced ? 'Advanced' : 'Basic'} mixed Discovery Skills preserves Sapho Truth-order through Semuta`, () => {
|
|
const f = fixture(advanced), frame = f.game.truthtrance!.order!;
|
|
assert.ok(viewGame(f.game, f.holder).saphoTruthOrderChoices.some(choice => choice.card === SAPHO && choice.mode === 'first'));
|
|
const pending = applyAction(f.game, f.holder, {type:'saphoTruthOrder', event:frame.event,
|
|
revision:frame.revision, card:SAPHO, mode:'first'});
|
|
assert.equal(pending.pendingTreacheryDiscard?.continuation.kind, 'saphoTruthOrderDiscard');
|
|
assert.equal(pending.pendingTreacheryDiscard?.reaction?.stage, 'offer');
|
|
assert.equal(pending.truthtrance, null);
|
|
const event = pending.pendingTreacheryDiscard!.batch.event;
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const view = viewGame(pending, 'richese');
|
|
view.players.find(row => row.id === 'richese')!.bot = difficulty;
|
|
const action = botActions(view).find(row => row.type === 'semutaCommit' || row.type === 'semutaPass');
|
|
assert.ok(action, difficulty);
|
|
assert.doesNotThrow(() => applyAction(pending, 'richese', action), difficulty);
|
|
}
|
|
for (const claim of [false, true]) {
|
|
let game = structuredClone(pending);
|
|
if (claim) game = applyAction(game, 'richese', {type:'semutaCommit', event});
|
|
else for (const player of game.players) game = applyAction(game, player.id, {type:'semutaPass', event});
|
|
if (game.pendingTreacheryDiscard?.reaction?.stage === 'select')
|
|
game = applyAction(game, 'richese', {type:'semutaSelect', event, card:SAPHO});
|
|
assert.equal(game.pendingTreacheryDiscard, null);
|
|
assert.equal(seat(game, 'richese').hand.some(card => card.id === SAPHO), claim);
|
|
assert.equal(seat(game, 'richese').hand.some(card => card.id === SEMUTA_DRUG_ID), !claim);
|
|
game = finishTruthOrder(game);
|
|
assert.equal(game.truthtrance!.queue[0].player, f.holder);
|
|
assert.deepEqual(game.order, f.parent.order);
|
|
assert.equal(game.turn, f.parent.turn); assert.equal(game.phase, f.parent.phase);
|
|
assert.equal(game.active, f.parent.active);
|
|
assert.deepEqual(game.movementRemaining, f.parent.movementRemaining);
|
|
game = finishOriginalTruthQuestions(game);
|
|
assert.equal(game.truthtrance, null);
|
|
assert.deepEqual(semutaSkillsInventory(game), f.inventory);
|
|
}
|
|
});
|