Files
Dune/tests/truth-fremen-special-karama-runtime.test.ts
T
Quintine 5b8d510470
Verify and publish checkpoint container / container (push) Successful in 10m22s
Support Fremen special Karama truth commitments
2026-10-29 18:26:33 +11:00

153 lines
9.1 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { createGame, joinGame, newPlayer, viewGame, type Action, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { windowCardOpportunity } from '../game/window-card-play';
import { actIxSpice as act, advanceIxSpice as advance, beforeIxSpiceDraw } from './fixture-ix-spice-cards';
import { truthBidInventory as inventory, truthBidPlayer as player } from './fixture-truth-bid';
/** Fresh Advanced classic setup and native first Spice Blow. Only original
* unopened deal positions are chosen; no played state, cards or forces are
* fabricated. The selected empty sand territory preserves the bounded preview. */
function fixture() {
const asker = 'harkonnen', owner = 'fremen', observer = 'atreides';
let game = createGame('TRUTHFREMENSPECIAL', newPlayer(asker, 'Harkonnen', 'harkonnen'), true, []);
joinGame(game, newPlayer(observer, 'Atreides', 'atreides'));
joinGame(game, newPlayer(owner, 'Fremen', 'fremen'));
for (const seat of game.players) game = act(game, seat.id, { type: 'ready' });
game = act(game, asker, { type: 'start', advancedPreview: true });
const originalCards = inventory(game);
const truths = game.deck.filter(card => card.effect === 'truthtrance');
const karama = game.deck.find(card => card.effect === 'karama');
const filler = game.deck.find(card => card.kind === 'shield');
assert.ok(truths.length === 2 && karama && filler);
for (const [position, id] of [truths[0].id, truths[1].id, filler.id, karama.id].entries()) {
const from = game.deck.findIndex(card => card.id === id); assert.ok(from >= position);
[game.deck[position], game.deck[from]] = [game.deck[from], game.deck[position]];
}
game = advance(game, state => beforeIxSpiceDraw(state));
assert.ok(player(game, asker).hand.some(card => card.id === truths[0].id));
assert.ok(player(game, owner).hand.some(card => card.id === karama.id));
assert.deepEqual(inventory(game), originalCards);
const opportunity = windowCardOpportunity(game, 'karamaFremenSpecial'); assert.ok(opportunity);
const territory = 'hagga_basin';
assert.ok(game.players.every(seat => Object.entries(seat.forces).every(([key, amount]) => !key.startsWith(`${territory}:`) || !amount)));
assert.ok(Object.entries(game.spice).every(([key, amount]) => !key.startsWith(`${territory}:`) || !amount));
return { game, asker, owner, observer, truth: truths[0].id, karama: karama.id,
territory, source: opportunity.source, originalCards };
}
function rejected(game: Game, actor: string, action: Action) {
const before = structuredClone(game); assert.throws(() => act(game, actor, action)); assert.deepEqual(game, before);
}
function declared(f: ReturnType<typeof fixture>) {
let game = act(f.game, f.asker, { type: 'card', card: f.truth });
while (game.truthtrance?.stage === 'priority') {
const seat = game.players.find(p => !game.truthtrance!.passed.includes(p.id)); assert.ok(seat);
game = act(game, seat.id, { type: 'truthPass' });
}
assert.equal(game.truthtrance?.stage, 'ask');
return game;
}
function question(f: ReturnType<typeof fixture>, target = f.owner) {
return { kind: 'turnActions', target, turn: f.game.turn, mixed: { kind: 'action', action: {
kind: 'windowCardPlay', effect: 'karamaFremenSpecial', source: f.source,
} } };
}
function asked(f: ReturnType<typeof fixture>) {
return act(declared(f), f.asker, { type: 'truthAsk', question: question(f) });
}
const special = (f: ReturnType<typeof fixture>): Action =>
({ type: 'card', mode: 'special', card: f.karama, territory: f.territory });
void test('Advanced Fremen special Yes spends the real Karama and summons the original empty-territory worm', () => {
const f = fixture(), questionState = asked(f);
assert.ok(viewGame(questionState, f.owner).truthTurnActionAnswers?.includes('yes'));
assert.equal(viewGame(questionState, f.observer).truthTurnActionAnswers, null);
let game = act(questionState, f.owner, { type: 'truthAnswer', answer: 'yes' });
const guide = viewGame(game, f.owner).turnActionGuide; assert.ok(guide);
assert.equal(guide.action.type, 'card'); assert.equal(guide.action.mode, 'special');
assert.equal(guide.action.card, f.karama);
for (const difficulty of DIFFICULTIES) {
const view = viewGame(game, f.owner); view.players.find(p => p.id === f.owner)!.bot = difficulty;
assert.deepEqual(botActions(view)[0], guide.action);
}
game = act(game, f.owner, special(f));
assert.equal(player(game, f.owner).specialKaramaUsed, true);
assert.ok(game.turnActionPromises!.find(row => row.player === f.owner)?.fulfilled);
assert.equal(game.turnActionPromises!.find(row => row.player === f.owner)?.windowCards!.karamaFremenSpecial!.played, true);
assert.equal(game.discard.filter(card => card.id === f.karama).length, 1);
assert.ok(game.summonedBeforeBlow || game.summonedWorm, 'The native summoned-worm sequence must actually begin');
assert.deepEqual(inventory(game), f.originalCards);
});
void test('Advanced Fremen special No blocks that role while retaining the actual physical card', () => {
const f = fixture(), state = asked(f);
assert.ok(viewGame(state, f.owner).truthTurnActionAnswers?.includes('no'));
const game = act(state, f.owner, { type: 'truthAnswer', answer: 'no' });
rejected(game, f.owner, special(f));
assert.ok(player(game, f.owner).hand.some(card => card.id === f.karama));
assert.notEqual(player(game, f.owner).specialKaramaUsed, true);
assert.deepEqual(inventory(game), f.originalCards);
});
void test('Fremen special source cannot bind another respondent or a forged turn', () => {
const f = fixture(), game = declared(f);
rejected(game, f.asker, { type: 'truthAsk', question: question(f, f.observer) });
const forged = question(f);
forged.mixed.action.source = JSON.stringify(['karamaFremenSpecial', game.turn + 1, f.owner]);
rejected(game, f.asker, { type: 'truthAsk', question: forged });
});
void test('a special summon cannot promise a future card that is not actually held', () => {
const f = fixture(), game = declared(f);
assert.ok(!player(game, f.owner).hand.some(card => card.effect === 'hajr'));
const mixed = { kind: 'and', terms: [question(f).mixed,
{ kind: 'action', action: { kind: 'cardPlay', effect: 'hajr' } }] };
const state = act(game, f.asker, { type: 'truthAsk', question: {
kind: 'turnActions', target: f.owner, turn: game.turn, mixed,
} });
assert.ok(!viewGame(state, f.owner).truthTurnActionAnswers?.includes('yes'));
assert.ok(viewGame(state, f.owner).truthTurnActionAnswers?.includes('no'));
rejected(state, f.owner, { type: 'truthAnswer', answer: 'yes' });
});
void test('a sole special-use Yes permits a populated sand territory and resolves the native worm protection', () => {
const original = fixture(), game = structuredClone(original.game), key = `${original.territory}:12`;
// Controlled physical positioning supplement: move one Fremen from its real
// Sietch Tabr starting group and one Atreides reserve counter into Hagga.
// This conserves both force pools but is not presented as an earned move.
// Swap a second physical Karama into Atreides' hand so its native response
// stays open instead of automatically passing when no counter is possible.
const fremen = player(game, original.owner), opponent = player(game, original.observer);
const counter = game.deck.findIndex(card => card.effect === 'karama' && card.id !== original.karama);
assert.ok(counter >= 0 && opponent.hand.length > 0);
[game.deck[counter], opponent.hand[0]] = [opponent.hand[0], game.deck[counter]];
assert.deepEqual(inventory(game), original.originalCards);
assert.ok((fremen.forces['sietch_tabr:14'] ?? 0) >= 1 && opponent.reserves >= 1);
fremen.forces['sietch_tabr:14'] -= 1;
fremen.forces[key] = (fremen.forces[key] ?? 0) + 1;
opponent.reserves -= 1;
opponent.forces[key] = (opponent.forces[key] ?? 0) + 1;
const f = { ...original, game };
let state = act(asked(f), f.owner, { type: 'truthAnswer', answer: 'yes' });
const opponentTanks = player(state, f.observer).tanks;
state = act(state, f.owner, special(f));
assert.equal(state.response?.kind, 'wormSurvival');
assert.equal(state.response?.owner, f.owner);
assert.equal(state.response?.location, f.territory);
assert.equal(player(state, f.owner).forces[key], 1);
assert.equal(player(state, f.observer).forces[key], 1);
assert.ok(state.turnActionPromises!.find(row => row.player === f.owner)?.fulfilled);
while (state.response?.kind === 'wormSurvival') {
const actor = state.players.find(p => !state.response!.passed.includes(p.id)); assert.ok(actor);
state = act(state, actor.id, { type: 'passResponse' });
}
assert.equal(player(state, f.owner).forces[key], 1, 'Fremen survive the allowed worm');
assert.equal(player(state, f.observer).forces[key] ?? 0, 0, 'The enemy force is devoured');
assert.equal(player(state, f.observer).tanks, opponentTanks + 1);
assert.equal(state.discard.filter(card => card.id === f.karama).length, 1);
assert.ok(state.summonedBeforeBlow || state.summonedWorm || state.nexus);
assert.deepEqual(inventory(state), f.originalCards);
});