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); });