React to a definite Truthtrance answer in the Homeworld Semuta envelope

This commit is contained in:
Quintine committed 2026-10-12 23:41:14 +11:00
1 parent f1706ab408
commit 38feaf17ba
10 files changed
+140 -14

No files matched your search

+46 -1
View File
@@ -1,6 +1,6 @@
import assert from 'node:assert/strict';
import {
applyAction, type Action, type Game,
applyAction, viewGame, type Action, type Game,
} from '../game/engine';
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
import { createSemutaHomeworldGuildIncomeFixture } from './fixture-semuta-homeworld-guild-income';
@@ -30,6 +30,51 @@ export function createSemutaHomeworldOrdinaryFixture(): SemutaHomeworldOrdinaryF
action: { type: 'card', card: hajr } };
}
export type SemutaHomeworldTruthtranceFixture = {
game: Game; holder: string; asker: string; questioned: string;
truthtrance: string; answer: Action;
};
/** Genuine original Homeworld occupation state; a real Truthtrance declaration,
* storm priority pass and definite fact answer produce the consumed card. The
* Truthtrance and the answering Shield are conserved physical cards. */
export function createSemutaHomeworldTruthtranceFixture(): SemutaHomeworldTruthtranceFixture {
const income = createSemutaHomeworldGuildIncomeFixture({ advanced: false });
let game = income.game;
assert.equal(game.phase, 5);
assert.equal(game.active, 'a');
const holder = income.holder, asker = 'a', questioned = income.owner;
const truthtrance = moveSemutaEmperorCard(game,
semutaEmperorCardId(game, card => card.effect === 'truthtrance'), asker);
const shieldIndex = game.deck.findIndex(card => card.name === 'Shield');
assert.ok(shieldIndex >= 0, 'A real Shield must remain in the original deck');
semutaEmperorPlayer(game, questioned).hand.push(game.deck.splice(shieldIndex, 1)[0]);
game = applyAction(game, asker, { type: 'card', card: truthtrance, cards: [] });
while (game.truthtrance?.stage === 'priority') {
const actor = game.players.find(player => !game.truthtrance!.passed.includes(player.id))!;
game = applyAction(game, actor.id, { type: 'truthPass' });
}
assert.equal(game.truthtrance?.stage, 'ask');
game = applyAction(game, game.truthtrance!.queue[0].player, {
type: 'truthAsk',
question: { kind: 'fact', target: questioned, fact: { kind: 'hand', name: 'Shield' } },
});
assert.equal(viewGame(game, questioned).truthAnswer, 'yes');
return { game, holder, asker, questioned, truthtrance,
answer: { type: 'truthAnswer', answer: 'yes' } };
}
export function reactSemutaHomeworldTruthtrance(
state: Game, fixture: SemutaHomeworldTruthtranceFixture, claim: boolean,
): Game {
const event = state.pendingTreacheryDiscard!.batch.event;
if (claim) return applyAction(state, fixture.holder, { type: 'semutaCommit', event });
let game = state;
for (const player of state.players)
game = applyAction(game, player.id, { type: 'semutaPass', event });
return game;
}
export type SemutaHomeworldDistransFixture = {
game: Game; holder: string; owner: string; distrans: string; given: string;
recipient: string; action: Action;
+56 -2
View File
@@ -6,8 +6,9 @@ import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
import { semutaEmperorPlayer } from './fixture-semuta-emperor-share';
import {
createSemutaHomeworldBoxFixture, createSemutaHomeworldDistransFixture,
createSemutaHomeworldOrdinaryFixture, reactSemutaHomeworldBox,
reactSemutaHomeworldDistrans, reactSemutaHomeworldOrdinary,
createSemutaHomeworldOrdinaryFixture, createSemutaHomeworldTruthtranceFixture,
reactSemutaHomeworldBox, reactSemutaHomeworldDistrans,
reactSemutaHomeworldOrdinary, reactSemutaHomeworldTruthtrance,
semutaHomeworldOrdinaryState,
} from './fixture-semuta-homeworld-ordinary';
@@ -158,6 +159,59 @@ for (const difficulty of ['Easy', 'Medium', 'Hard', 'Brutal'] as const) void tes
},
);
for (const claim of [false, true]) void test(
`Homeworld Truthtrance answer Semuta ${claim ? 'claims' : 'passes'} the consumed card after the public answer`, () => {
const f = createSemutaHomeworldTruthtranceFixture();
const answered = applyAction(f.game, f.questioned, f.answer);
const pending = answered.pendingTreacheryDiscard;
assert.ok(pending, 'A definite Truthtrance answer must offer its printed Semuta cost');
assert.equal(pending.batch.cause, 'truthtrance');
assert.deepEqual(pending.batch.entries.map(entry => ({
id: entry.card.id, by: entry.discardedBy, face: entry.publicFace,
})), [{ id: f.truthtrance, by: f.asker, face: true }]);
assert.equal(answered.truthHistory!.length, 1);
assert.equal(answered.truthHistory![0].answer, 'yes');
assert.equal(answered.truthtrance, null);
for (const player of answered.players) viewGame(answered, player.id);
const before = semutaHomeworldOrdinaryState(answered);
const event = pending.batch.event;
assert.throws(() => applyAction(answered, f.holder, { type: 'semutaSelect', event, card: f.truthtrance }));
const done = reactSemutaHomeworldTruthtrance(answered, f, claim);
assert.equal(done.pendingTreacheryDiscard, null);
assert.equal(done.resolvedTreacheryDiscardSequence, pending.sequence);
const holder = semutaEmperorPlayer(done, f.holder);
assert.equal(holder.hand.some(card => card.id === f.truthtrance), claim);
assert.equal(holder.hand.some(card => card.id === SEMUTA_DRUG_ID), !claim);
assert.equal(done.discard.some(card => card.id === f.truthtrance), !claim);
assert.deepEqual(done.truthHistory, answered.truthHistory,
'The published answer history is not rewritten by the reaction');
assert.deepEqual(semutaHomeworldOrdinaryState(done), before,
'The answer, wallets and occupation authority are unchanged');
assert.throws(() => applyAction(done, f.questioned, f.answer));
const suffix = applyAction(done, f.asker, { type: 'endMovement' });
assert.ok(!suffix.movementRemaining?.includes(f.asker));
assert.equal(suffix.phase, 5);
},
);
for (const difficulty of ['Easy', 'Medium', 'Hard', 'Brutal'] as const) void test(
`the ${difficulty} bot settles the Homeworld Truthtrance Semuta cost`, () => {
const f = createSemutaHomeworldTruthtranceFixture();
const answered = applyAction(f.game, f.questioned, f.answer);
const view = viewGame(answered, f.holder);
view.players.find(player => player.id === f.holder)!.bot = difficulty;
const action = botActions(view)[0];
assert.ok(action);
const done = applyAction(answered, f.holder, action);
assert.equal(done.pendingTreacheryDiscard, null);
const holder = done.players.find(player => player.id === f.holder)!;
const claimed = holder.hand.some(card => card.id === f.truthtrance);
assert.equal(claimed || done.discard.some(card => card.id === f.truthtrance), true);
assert.equal(holder.hand.some(card => card.id === SEMUTA_DRUG_ID), !claimed);
assert.deepEqual(done.truthHistory, answered.truthHistory);
},
);
void test('a full-hand Homeworld Richese exchanges Semuta for the used ordinary card', () => {
const f = createSemutaHomeworldOrdinaryFixture();
const holder = semutaEmperorPlayer(f.game, f.holder);