Files
Dune/tests/truthtrance-no-field-shipment-runtime.test.ts
T
Quintine 7fccd62814
Verify and publish checkpoint container / container (push) Canceled after 0s
Support Truthtrance No-Field shipment promises
2026-10-29 10:07:00 +11:00

242 lines
12 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { deployRicheseNoField } from '../game/richese-no-field';
import type { TruthQuestion } from '../game/truthtrance';
import {
createTruthtranceRichesePromisesFixture as fixture,
actTruthtranceRichesePromises as act,
askTruthtranceRichesePromises as ask,
advanceTruthtranceRichesePromises as advance,
truthtranceRichesePromisesClean as clean,
truthtranceRichesePromisesPlayer as player,
truthtranceRichesePromisesInventory as inventory,
} from './fixture-truthtrance-richese-promises';
const markerQuestion = {
kind: 'shipment', target: 'richese',
claim: {kind: 'noField', territory: 'carthag', sector: 11},
} as unknown as TruthQuestion;
const reload = (game: Game): Game => JSON.parse(JSON.stringify(game));
const seat = (game: Game, id: string) => {
const found = game.players.find(row => row.id === id); assert.ok(found); return found;
};
function reject(game: Game, actor: string, action: Action) {
const before = structuredClone(game);
assert.throws(() => applyAction(game, actor, action));
assert.deepEqual(game, before);
}
function start(answer: 'yes' | 'no') {
const f = fixture();
const question = ask(structuredClone(f.game), f, markerQuestion);
assert.ok(viewGame(question, f.asker).truthShipmentTargets.includes(f.target));
assert.deepEqual(viewGame(question, f.target).truthShipmentAnswers, ['yes', 'no']);
let game = act(question, f.target, {type:'truthAnswer', answer});
game = advance(game, clean);
assert.equal(game.shipmentPromises?.length, 1);
assert.equal(game.shipmentPromises![0].answer, answer === 'yes');
assert.equal(game.shipmentPromises![0].fulfilled, undefined);
assert.equal(game.shipmentPromises![0].released, undefined);
assert.equal(game.discard.filter(card => card.id === f.truthtrance).length, 1);
game = advance(game, state => state.phase === 5 && state.active === f.target && clean(state));
return {f, game};
}
function markerAction(game: Game, value = 0, territory = 'carthag', sector = 11): Action {
const richese = player(game);
const token = richese.noField!.tokens.find(item => item.value === value); assert.ok(token);
assert.ok(richese.noFieldEvent);
return {type:'ship', noField:token.id, event:richese.noFieldEvent, territory, sector, allyPayment:0};
}
function finishResponse(game: Game): Game {
return advance(game, clean);
}
void test('Basic early Yes requires the requested owned No-Field, including its zero token', () => {
const {f, game: before} = start('yes');
const choice = markerAction(before), target = player(before), marker = String(choice.noField);
const ownView = viewGame(before, f.target), otherView = viewGame(before, f.asker);
assert.ok(ownView.shipmentCompletion?.actions.some(action => action.type === 'ship' && action.noField));
assert.equal(otherView.shipmentCompletion, null);
assert.equal(otherView.richeseNoField?.private, null);
assert.equal(JSON.stringify(otherView).includes(marker), false);
reject(before, f.target, {type:'endMovement'});
reject(before, f.target, {type:'ship', territory:'carthag', sector:11, amount:1, elite:0, allyPayment:0});
reject(before, f.target, markerAction(before, 0, 'arrakeen', 10));
const declared = act(reload(before), f.target, choice);
const done = finishResponse(declared);
assert.equal(player(done).noField?.deployed?.tokenId, marker);
assert.deepEqual(player(done).noField?.deployed?.location, {territory:'carthag', sector:11});
assert.equal(player(done).reserves, target.reserves);
assert.equal(player(done).spice, target.spice - 1);
assert.equal(player(done).shipped, true);
assert.equal(done.shipmentPromises![0].fulfilled, true);
assert.equal(done.shipmentPromises![0].released, undefined);
assert.deepEqual(inventory(done), f.originalInventory);
assert.equal(viewGame(done, f.asker).richeseNoField?.private, null);
reject(done, f.target, choice);
});
void test('Basic No forbids the selected marker shipment but permits a physical shipment', () => {
const {f, game: before} = start('no');
reject(before, f.target, markerAction(before));
const done = finishResponse(act(before, f.target, {
type:'ship', territory:'carthag', sector:11, amount:1, elite:0, allyPayment:0,
}));
assert.equal(player(done).forces['carthag:11'], 1);
assert.equal(player(done).noField?.deployed, null);
assert.equal(done.shipmentPromises![0].fulfilled, true);
assert.deepEqual(inventory(done), f.originalInventory);
});
void test('Basic No permits a marker in a different destination', () => {
const {f, game: before} = start('no');
const action = markerAction(before, 3, 'arrakeen', 10);
const done = finishResponse(act(before, f.target, action));
assert.deepEqual(player(done).noField?.deployed?.location, {territory:'arrakeen', sector:10});
assert.equal(done.shipmentPromises![0].fulfilled, true);
assert.deepEqual(inventory(done), f.originalInventory);
});
void test('an opposing Karama prevents the marker and releases an impossible Yes without spending shipment', () => {
const {f, game: original} = start('yes');
const game = structuredClone(original), opponent = seat(game, f.observer);
const from = game.deck.findIndex(card => card.effect === 'karama');
assert.ok(from >= 0 && opponent.hand.length);
// Move an existing physical Karama into the observer's hand via a conserved
// card swap; the declaration and the resulting counter are actual actions.
[opponent.hand[0], game.deck[from]] = [game.deck[from], opponent.hand[0]];
const karama = opponent.hand[0].id, before = structuredClone(game);
const declared = act(game, f.target, markerAction(game));
assert.equal(declared.response?.kind, 'richeseNoField');
assert.equal(declared.shipmentPromises![0].fulfilled, undefined);
let stopped = act(declared, f.observer, {type:'card', mode:'cancel', card:karama});
stopped = finishResponse(stopped);
assert.equal(player(stopped).noField?.deployed, null);
assert.equal(player(stopped).noFieldBlockedTurn, stopped.turn);
assert.equal(player(stopped).shipped, false);
assert.equal(player(stopped).spice, player(before).spice);
assert.equal(stopped.shipmentPromises![0].released, true);
assert.equal(stopped.shipmentPromises![0].fulfilled, undefined);
reject(stopped, f.target, markerAction(stopped));
const normal = finishResponse(act(stopped, f.target, {
type:'ship', territory:'carthag', sector:11, amount:1, elite:0, allyPayment:0,
}));
assert.equal(player(normal).forces['carthag:11'], 1);
assert.deepEqual(inventory(normal), f.originalInventory);
});
void test('all existing bot profiles use the same private feasible answers and legal marker completion', () => {
const f = fixture(), asked = ask(structuredClone(f.game), f, markerQuestion);
for (const difficulty of DIFFICULTIES) {
const answering = viewGame(asked, f.target);
answering.players.find(row => row.id === f.target)!.bot = difficulty;
const answer = botActions(answering).find(action => action.type === 'truthAnswer');
assert.ok(answer && answering.truthShipmentAnswers?.includes(answer.answer as 'yes' | 'no' | 'unknown'));
assert.doesNotThrow(() => act(reload(asked), f.target, answer));
const {game} = start('yes');
const turn = viewGame(game, f.target);
turn.players.find(row => row.id === f.target)!.bot = difficulty;
const action = botActions(turn)[0]; assert.ok(action, difficulty);
assert.doesNotThrow(() => act(reload(game), f.target, action));
}
});
void test('a marker-only declaration does not satisfy an existing physical-force promise', () => {
const f = fixture();
let game = ask(structuredClone(f.game), f);
game = act(game, f.target, {type:'truthAnswer', answer:'yes'});
game = advance(game, state => state.phase === 5 && state.active === f.target && clean(state));
reject(game, f.target, markerAction(game));
const done = finishResponse(act(game, f.target, f.shipmentAction));
assert.equal(player(done).forces['carthag:11'], 5);
assert.equal(done.shipmentPromises![0].fulfilled, true);
});
void test('Basic cannot promise a single shipment is both a marker and physical-only forces', () => {
const f = fixture();
const question = {
kind:'shipment', target:f.target, claim:{op:'and', terms:[
{kind:'noField', territory:'carthag'}, {territory:'carthag', minimum:1},
]},
} as unknown as TruthQuestion;
const asked = ask(structuredClone(f.game), f, question);
assert.deepEqual(viewGame(asked, f.target).truthShipmentAnswers, ['no']);
reject(asked, f.target, {type:'truthAnswer', answer:'yes'});
const done = act(asked, f.target, {type:'truthAnswer', answer:'no'});
assert.equal(done.shipmentPromises![0].answer, false);
});
void test('the marker question cannot target a faction that does not own a No-Field', () => {
const f = fixture();
let game = act(structuredClone(f.game), f.asker, {type:'card', card:f.truthtrance});
game = advance(game, state => state.truthtrance?.stage === 'ask');
const question = {kind:'shipment', target:f.observer, claim:{kind:'noField', territory:'carthag'}};
reject(game, f.asker, {type:'truthAsk', question});
});
void test('a deployed marker can be revealed before the promised different No-Field is shipped', () => {
const f = fixture(), staged = structuredClone(f.game), richese = player(staged);
const previous = richese.noField!.tokens.find(item => item.value === 3); assert.ok(previous);
// Model an earlier turn's original marker with the same physical token and
// history; staging does not create forces or change reserves.
richese.noField = deployRicheseNoField(richese.noField!, {
tokenId: previous.id, controller: richese.id,
location: {territory:'polar_sink', sector:0},
});
let game = ask(staged, f, markerQuestion);
assert.ok(viewGame(game, f.target).truthShipmentAnswers?.includes('yes'));
game = act(game, f.target, {type:'truthAnswer', answer:'yes'});
game = advance(game, state => state.phase === 5 && state.active === f.target && clean(state));
const first = viewGame(game, f.target).shipmentCompletion?.actions[0];
assert.equal(first?.type, 'revealNoField');
assert.equal(first?.token, previous.id);
game = act(game, f.target, first!);
assert.equal(player(game).noField?.deployed, null);
assert.equal(player(game).noField?.lastShipped, previous.id);
assert.equal(player(game).forces['polar_sink:0'], 3);
const next = viewGame(game, f.target).shipmentCompletion?.actions[0];
assert.equal(next?.type, 'ship');
assert.ok(next?.noField && next.noField !== previous.id);
game = finishResponse(act(game, f.target, next!));
assert.equal(game.shipmentPromises![0].fulfilled, true);
assert.equal(player(game).noField?.deployed?.tokenId, next!.noField);
assert.deepEqual(player(game).noField?.deployed?.location, {territory:'carthag', sector:11});
});
function givePhysicalKarama(game: Game, id: string): string {
const target = seat(game, id), held = target.hand.find(card => card.effect === 'karama');
if (held) return held.id;
assert.ok(target.hand.length);
const inDeck = game.deck.findIndex(card => card.effect === 'karama');
if (inDeck >= 0) {
[target.hand[0], game.deck[inDeck]] = [game.deck[inDeck], target.hand[0]];
return target.hand[0].id;
}
for (const other of game.players) {
if (other.id === id) continue;
const index = other.hand.findIndex(card => card.effect === 'karama');
if (index >= 0) {
[target.hand[0], other.hand[index]] = [other.hand[index], target.hand[0]];
return target.hand[0].id;
}
}
throw Error('No original physical Karama was available for the fixture');
}
void test('Richese cannot cancel its own promised No-Field while another responder holds Karama', () => {
const game = start('yes').game, karama = givePhysicalKarama(game, 'richese');
const opposing = givePhysicalKarama(game, 'emperor');
assert.notEqual(karama, opposing);
assert.ok(player(game).hand.some(card => card.id === karama));
const declared = act(game, 'richese', markerAction(game));
assert.equal(declared.response?.kind, 'richeseNoField');
reject(declared, 'richese', {type:'card', mode:'cancel', card:karama});
assert.equal(declared.pendingKull ?? null, null);
assert.equal(declared.pendingTreacheryDiscard ?? null, null);
assert.ok(player(declared).hand.some(card => card.id === karama));
assert.equal(declared.shipmentPromises![0].released, undefined);
assert.equal(declared.shipmentPromises![0].fulfilled, undefined);
});