Files
Dune/tests/richese-reveal-fremen-grant-runtime.test.ts
T
Quintine 82d857b667
Verify and publish checkpoint container / container (push) Canceled after 0s
Allow Richese reveal during selected responses
2026-10-29 07:58:48 +11:00

114 lines
7.2 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 { actFremenRevivalGrant as act, nextFremenRevivalGrantStep, fremenRevivalGrantClean as clean } from './fixture-fremen-revival-grant';
import { truthMovementInventory as inventory } from './fixture-truth-movement';
const owner = 'richese', fremen = 'fremen', ally = 'atreides', observer = 'emperor';
const player = (game: Game, id: string) => {
const found = game.players.find(row => row.id === id); assert.ok(found); return found;
};
function next(game: Game) {
// Richese pays for the real marker during its original first movement turn.
if (game.turn === 1 && game.phase === 5 && game.active === owner && clean(game)) {
const richese = player(game, owner);
if (!richese.shipped) {
const marker = richese.noField!.tokens.find(token => token.value === 3); assert.ok(marker);
return {actor:owner, action:{type:'ship', noField:marker.id, event:richese.noFieldEvent,
territory:'polar_sink', sector:0, allyPayment:0} satisfies Action};
}
return {actor:owner, action:{type:'endMovement'} satisfies Action};
}
return nextFremenRevivalGrantStep(game);
}
function reach(game: Game, until: (state: Game) => boolean): Game {
for (let count = 0; count < 2000; count++) {
if (until(game)) return game;
assert.notEqual(game.status, 'finished', 'The native game ended before its grant response.');
const step = next(game); assert.ok(step, `Supply native action at ${game.turn}/${game.phase}/${game.decision?.kind}`);
game = act(game, step.actor, step.action);
}
throw Error('The native Fremen grant response was not reached.');
}
/** Genuine Basic game: native starting components, real Richese paid shipment,
* Atreides invasion and Fremen battle losses, a subsequent Shai-Hulud and
* Fremen/Atreides alliance. Only original unopened cards and scalar lotteries
* are selected; no board force, wallet, alliance, response or casualty is staged. */
function nativeGrantSource() {
let game = createGame('RICHESEFREMENGRANT', newPlayer(fremen, 'Fremen', 'fremen'), false, ['choam']);
joinGame(game, newPlayer(ally, 'Atreides', 'atreides'));
joinGame(game, newPlayer('choam', 'CHOAM', 'choam'));
joinGame(game, newPlayer(owner, 'Richese', 'richese'));
joinGame(game, newPlayer(observer, 'Emperor', 'emperor'));
for (const seat of game.players) game = act(game, seat.id, {type:'ready'});
game = act(game, game.host, {type:'start'});
assert.equal(game.status, 'setup');
const ordinary = game.deck.filter(card => ['projectile','poison','shield','snooper'].includes(card.kind)).slice(0, 4);
const karama = game.deck.find(card => card.effect === 'karama');
assert.equal(ordinary.length, 4); assert.ok(karama);
for (const [position, card] of [...ordinary, karama].entries()) {
const from = game.deck.findIndex(candidate => candidate.id === card.id); assert.ok(from >= position);
[game.deck[position], game.deck[from]] = [game.deck[from], game.deck[position]];
}
game = reach(game, state => state.status === 'playing');
assert.ok(player(game, observer).hand.some(card => card.id === karama.id));
for (const position of [0,1]) {
const from = game.spiceDeck.findIndex((card, index) => index >= position && 'territory' in card &&
!card.discovery && card.territory !== 'sietch_tabr');
assert.ok(from >= position);
[game.spiceDeck[position], game.spiceDeck[from]] = [game.spiceDeck[from], game.spiceDeck[position]];
}
game = reach(game, state => state.turn === 1 && state.phase === 5 && state.active === ally && clean(state));
game = act(game, ally, {type:'ship', territory:'sietch_tabr', sector:14, amount:6, elite:0, allyPayment:0});
game = reach(game, state => state.turn === 1 && state.phase === 6 && clean(state) && !!state.active);
const choice = viewGame(game, game.active!).battleChoices.find(row => row.territory === 'sietch_tabr'); assert.ok(choice);
game = act(game, choice.chooser, {type:'chooseBattle', territory:'sietch_tabr',
target:choice.attacker === choice.chooser ? choice.defender : choice.attacker});
for (const actor of [fremen, ally]) {
game = reach(game, state => !!state.battle && clean(state) && !state.battle.preparation && state.battle.preLeader?.closed !== false);
const leaders = player(game, actor).leaders.filter(leader => !leader.dead && !leader.usedAt)
.sort((a,b) => a.strength - b.strength || a.id.localeCompare(b.id));
const leader = actor === fremen ? leaders.at(-1) : leaders[0]; assert.ok(leader);
game = act(game, actor, {type:'battlePlan', leader:leader.id, dial:actor === fremen ? 6 : 0,
support:0, weapon:null, defense:null});
}
game = reach(game, state => !state.battle && clean(state));
assert.equal(player(game, ally).tanks, 6);
const marker = player(game, owner).noField?.deployed; assert.ok(marker);
const event = player(game, owner).noFieldEvent; assert.ok(event);
game = reach(game, state => state.turn === 2 && state.phase === 1 && clean(state) &&
!state.spiceSequence && !state.spiceResolution && !state.spiceWindow && !state.nexus && state.ready.length === 0);
const worm = game.spiceDeck.findIndex(card => 'worm' in card && !card.greatMaker && !card.suppressed && !card.thumper);
assert.ok(worm >= 0); game.spiceDeck.unshift(game.spiceDeck.splice(worm,1)[0]);
game = reach(game, state => !!state.nexus && !state.spiceWindow && !state.spiceResolution && clean(state));
game = act(game, fremen, {type:'alliance', target:ally});
game = act(game, ally, {type:'alliance', target:fremen});
assert.equal(player(game, fremen).ally, ally); assert.equal(player(game, ally).ally, fremen);
game = reach(game, state => state.turn === 2 && state.phase === 4 && clean(state));
game = act(game, fremen, {type:'grantRevival'});
assert.equal(game.response?.kind, 'fremenRevivalGrant');
assert.ok(player(game, observer).hand.some(card => card.id === karama.id));
return {game, token:marker.tokenId, event, cards:inventory(game)};
}
void test('voluntary No-Field reveal preserves the real Fremen ally grant response and revival continuation', () => {
const {game,token,event,cards} = nativeGrantSource(), response = structuredClone(game.response),
reserves = player(game, owner).reserves, field = player(game, owner).forces['polar_sink:0'] ?? 0,
tanks = player(game, ally).tanks;
assert.equal(viewGame(game, owner).richeseNoField?.canReveal, true);
const after = act(game, owner, {type:'revealNoField', token, event});
assert.deepEqual(after.response, response);
assert.equal(player(after, owner).noField?.deployed, null);
assert.equal(player(after, owner).reserves, reserves - 3);
assert.equal(player(after, owner).forces['polar_sink:0'], field + 3);
assert.deepEqual(inventory(after), cards);
const control = act(game, observer, {type:'passResponse'});
const continued = act(after, observer, {type:'passResponse'});
assert.notEqual(continued.response?.kind, 'fremenRevivalGrant');
assert.deepEqual(continued.freeRevival, control.freeRevival);
assert.equal(player(continued, ally).tanks, tanks);
assert.equal(player(continued, owner).forces['polar_sink:0'], field + 3);
assert.deepEqual(inventory(continued), cards);
});