114 lines
5.6 KiB
TypeScript
114 lines
5.6 KiB
TypeScript
import assert from 'node:assert/strict';
|
||
import test from 'node:test';
|
||
import { applyAction, viewGame, type Game } from '../game/engine';
|
||
import { actChoamCharityFunding, advanceChoamCharityFunding, createChoamCharityFundingFixture,
|
||
nextChoamCharityFundingStep } from './fixture-choam-charity-funding';
|
||
|
||
let fixture: ReturnType<typeof createChoamCharityFundingFixture> | undefined;
|
||
const source = () => fixture ??= createChoamCharityFundingFixture();
|
||
const owner = 'choam', observer = 'atreides';
|
||
const seat = (g: Game, id: string) => {
|
||
const player = g.players.find(p => p.id === id); assert.ok(player); return player;
|
||
};
|
||
const step = (g: Game) => {
|
||
const next = nextChoamCharityFundingStep(g); assert.ok(next);
|
||
return actChoamCharityFunding(g, next.actor, next.action);
|
||
};
|
||
const rejected = (g: Game, id: string, action: {type:string; [key:string]:unknown}) => {
|
||
const before = structuredClone(g);
|
||
assert.throws(() => applyAction(g, id, action));
|
||
assert.deepEqual(g, before);
|
||
};
|
||
|
||
/** Public Basic CHOAM/Atreides/Guild game, with the fixture's original deal
|
||
* and genuine setup and Storm actions; stop at the actual first closing market. */
|
||
function firstMarket() {
|
||
const g = advanceChoamCharityFunding(structuredClone(source().afterSetup), state =>
|
||
state.turn === 1 && state.phase === 0 && state.decision?.kind === 'choamMarket');
|
||
assert.deepEqual(g.expansions, ['choam']);
|
||
assert.equal(g.advanced, false);
|
||
assert.equal(g.choamMarket?.owner, owner);
|
||
assert.equal(g.choamMarket.resume, 'storm');
|
||
return g;
|
||
}
|
||
function optIn(g: Game) {
|
||
return actChoamCharityFunding(g, owner, {type:'decision', done:true, skipMarketsThisTurn:true});
|
||
}
|
||
|
||
void test('a human CHOAM can waive remaining markets this turn and the native Storm continuation proceeds', () => {
|
||
const before = firstMarket();
|
||
assert.equal(viewGame(before, owner).choamMarketSkipAvailable, true);
|
||
assert.equal(viewGame(before, owner).choamMarketSkipActive, false);
|
||
assert.equal(viewGame(before, observer).choamMarketSkipAvailable, false);
|
||
assert.equal(viewGame(before, observer).choamMarketSkipActive, false);
|
||
const game = optIn(before);
|
||
assert.equal(game.turn, 1);
|
||
assert.equal(game.phase, 1);
|
||
assert.equal(game.choamMarket, null);
|
||
assert.equal(viewGame(game, owner).choamMarketSkipActive, true);
|
||
assert.equal(viewGame(game, observer).choamMarketSkipActive, false);
|
||
let next = JSON.parse(JSON.stringify(game)) as Game;
|
||
for (let count = 0; next.turn === 1 && next.phase < 7 && count < 800; count++) {
|
||
assert.notEqual(next.decision?.kind, 'choamMarket', `Unexpected market in phase ${next.phase}`);
|
||
next = step(next);
|
||
}
|
||
assert.equal(next.turn, 1);
|
||
assert.equal(next.phase, 7);
|
||
assert.equal(viewGame(next, owner).choamMarketSkipActive, true);
|
||
assert.notEqual(next.decision?.kind, 'choamMarket');
|
||
});
|
||
|
||
void test('waiver continuation does not inspect or depend on CHOAM’s hidden card faces', () => {
|
||
const original = optIn(firstMarket());
|
||
const changed = structuredClone(original);
|
||
const choam = seat(changed, owner), guild = seat(changed, 'guild');
|
||
assert.ok(choam.hand.length && guild.hand.length);
|
||
// Swap two actual held cards, preserving all public counts and custody.
|
||
[choam.hand[0], guild.hand[0]] = [guild.hand[0], choam.hand[0]];
|
||
assert.deepEqual(viewGame(original, observer), viewGame(changed, observer));
|
||
for (let game of [original, changed]) {
|
||
for (let count = 0; game.phase === 1 && count < 100; count++) {
|
||
assert.notEqual(game.decision?.kind, 'choamMarket');
|
||
game = step(game);
|
||
}
|
||
assert.equal(game.phase, 2);
|
||
assert.notEqual(game.decision?.kind, 'choamMarket');
|
||
}
|
||
});
|
||
|
||
void test('only CHOAM can opt in or revoke, and revocation restores the next real market', () => {
|
||
const market = firstMarket();
|
||
rejected(market, observer, {type:'decision', done:true, skipMarketsThisTurn:true});
|
||
rejected(market, owner, {type:'cancelChoamMarketSkip'});
|
||
let game = optIn(market);
|
||
rejected(game, observer, {type:'cancelChoamMarketSkip'});
|
||
game = actChoamCharityFunding(game, owner, {type:'cancelChoamMarketSkip'});
|
||
assert.equal(viewGame(game, owner).choamMarketSkipActive, false);
|
||
rejected(game, owner, {type:'cancelChoamMarketSkip'});
|
||
game = advanceChoamCharityFunding(game, state => state.phase === 1 && state.decision?.kind === 'choamMarket');
|
||
assert.equal(game.choamMarket?.owner, owner);
|
||
assert.equal(viewGame(game, owner).choamMarketSkipAvailable, true);
|
||
game = actChoamCharityFunding(game, owner, {type:'decision', done:true});
|
||
assert.equal(viewGame(game, owner).choamMarketSkipActive, false);
|
||
});
|
||
|
||
for (const mode of ['bot','autopilot'] as const) void test(
|
||
`the standing waiver does not skip a market when CHOAM is under ${mode} control`, () => {
|
||
let game = optIn(firstMarket());
|
||
// A labelled staged controller handover; all game actions before it were native.
|
||
seat(game, owner)[mode] = 'Easy';
|
||
game = advanceChoamCharityFunding(game, state => state.phase === 1 && state.decision?.kind === 'choamMarket');
|
||
assert.equal(game.choamMarket?.owner, owner);
|
||
},
|
||
);
|
||
|
||
void test('the waiver expires at the next turn and preserves CHOAM’s final Mentat decision', () => {
|
||
let game = optIn(firstMarket());
|
||
game = advanceChoamCharityFunding(game, state => state.turn === 1 && state.decision?.kind === 'choamMentat');
|
||
assert.equal(game.phase, 8);
|
||
assert.equal(game.decision?.player, owner);
|
||
game = advanceChoamCharityFunding(game, state => state.turn === 2 && state.phase === 0 && state.decision?.kind === 'choamMarket');
|
||
assert.equal(viewGame(game, owner).choamMarketSkipActive, false);
|
||
assert.equal(viewGame(game, owner).choamMarketSkipAvailable, true);
|
||
});
|