114 lines
7.2 KiB
TypeScript
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);
|
|
});
|