Files
Dune/tests/richese-reveal-interruption-quote.test.ts
T
Quintine 9ab9b9a613
Verify and publish checkpoint container / container (push) Successful in 11m25s
Expand Basic Richese and Terror interactions
2026-10-29 09:59:45 +11:00

190 lines
11 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { createGame, newPlayer, type Decision, type Game } from '../game/engine';
import { richeseRevealInterruptSupported as supported } from '../game/richese-reveal-interruption';
/** Staged parent records test only the admission quote; native parent and
* physical token transitions are exercised by the separate runtime suite. */
function source(): Game {
const g = createGame('REVEALQUOTE', newPlayer('r', 'Richese', 'richese'), false, ['choam', 'ix']);
g.players.push(newPlayer('c', 'CHOAM', 'choam'), newPlayer('e', 'Emperor', 'emperor'));
g.status = 'playing'; g.phase = 3; g.turn = 2;
return g;
}
function parent(kind: Decision['kind']): Game {
const g = source();
if (kind === 'auctionPayment') {
g.decision = {kind, player:'e'};
g.auction = {cards:[], index:0, bid:3, bidder:'e', active:'e', passed:[], opener:0};
} else if (kind === 'richeseDeclaration' || kind === 'richeseCacheTerms' || kind === 'richeseCache' || kind === 'richeseUnbid') {
g.decision = {kind, player:'r'};
g.richeseBidding = {owner:'r', event:'round', turn:g.turn,
stage:kind === 'richeseDeclaration' ? 'declaration' : kind === 'richeseUnbid' ? 'lot' : 'cacheOffer',
position:'first', normalCount:1, blackMarketSold:false, cacheCanceled:false};
if (kind === 'richeseUnbid') g.richeseAuction = {
event:'lot', cardId:'card', source:'cache', owner:'r', method:'onceAround', eligible:['r','e'],
order:['e','r'], tieOrder:['r','e'], active:null, bid:0, bidder:null, passed:['r','e'], acted:['r','e'], sealed:{}, outcome:{kind:'unbid'},
};
} else if (kind === 'choamMarket' || kind === 'choamTradeReply' || kind === 'choamTradeConfirm') {
g.decision = {kind, player:kind === 'choamTradeReply' ? 'r' : 'c'};
g.choamMarket = {owner:'c', resume:'phase', blocked:[],
...(kind === 'choamMarket' ? {} : {trade:{ally:'r', offered:'offered',
...(kind === 'choamTradeConfirm' ? {returned:'returned'} : {})}})};
} else if (kind === 'richeseAllyOpportunity') {
g.phase = 5;
g.decision = {kind, player:'r', recipient:'e'};
g.richeseAllyOpportunity = {turn:g.turn, recipient:'e'};
} else throw Error(`Unrecognized staged parent ${kind}`);
return g;
}
void test('each supported economic parent is admitted without modifying its frame or inspecting marker custody', () => {
for (const kind of ['auctionPayment','richeseDeclaration','richeseCacheTerms','richeseCache','richeseUnbid',
'choamMarket','choamTradeReply','choamTradeConfirm','richeseAllyOpportunity'] as const) {
const game = parent(kind), before = structuredClone(game);
assert.equal(supported(game), true, kind);
assert.deepEqual(game, before);
assert.equal(game.players[0].noField?.deployed ?? null, null, 'Seat/marker authorization belongs to the caller');
}
});
void test('a clean Ix phase opening remains admitted before or after its owner passes', () => {
const game = source(); game.phase = 0; game.phaseOpening = {passed:[], initialize:true}; game.amalWindowOpen = true;
assert.equal(supported(game), true);
game.phaseOpening.passed.push('r');
assert.equal(supported(game), true);
const before = structuredClone(game); assert.equal(supported(game), true); assert.deepEqual(game, before);
game.decision = {kind:'auctionPayment', player:'e'}; assert.equal(supported(game), false);
game.decision = null; game.amalWindowOpen = false; assert.equal(supported(game), false);
});
void test('supported native responses admit the reveal without changing their original source or passes', () => {
const charity = source(); charity.phase = 2;
charity.response = {kind:'choamCharity', owner:'c', passed:['e']};
const auction = source(); auction.players.push(newPlayer('a','Atreides','atreides'));
auction.auction = {cards:[{id:'lot', name:'Shield', kind:'shield'}], index:0, bid:0,
bidder:null, active:'a', passed:[], opener:0};
auction.response = {kind:'atreidesAuction', owner:'a', passed:['e']};
const declaration = source();
declaration.richeseCache = [];
declaration.richeseBidding = {owner:'r', event:'round', turn:declaration.turn,
stage:'declaration', position:'first', normalCount:1, normalCountBeforeCache:1,
blackMarketSold:false, cacheCanceled:false};
declaration.response = {kind:'richeseAuction',owner:'r',passed:['e']};
const grant = source(); grant.phase = 4;
grant.players.push(newPlayer('f','Fremen','fremen'));
grant.players.find(p => p.id === 'f')!.ally = 'r'; grant.players.find(p => p.id === 'r')!.ally = 'f';
grant.freeRevival = [];
grant.response = {kind:'fremenRevivalGrant', owner:'f', recipient:'r', passed:['e'],
fremenGrant:{turn:grant.turn, owner:'f', recipient:'r'}};
for (const game of [charity, auction, declaration, grant]) {
const before = structuredClone(game);
assert.equal(supported(game), true, game.response!.kind);
assert.deepEqual(game, before);
}
});
void test('response admission requires the original phase, owner, source and no nested interaction', () => {
const charity = source(); charity.phase = 2; charity.response = {kind:'choamCharity',owner:'c',passed:[]};
for (const patch of [
{phase:3}, {decision:{kind:'auctionPayment',player:'e'}}, {phaseOpening:{passed:[],initialize:true}},
{pendingShipment:{}}, {response:{kind:'choamCharity',owner:'e',passed:[]}},
{response:{kind:'choamCharity',owner:'c',passed:['unknown']}},
{response:{kind:'choamCharity',owner:'c',passed:['e','e']}},
{response:{kind:'choamCharity',owner:'c',passed:[],intent:'extra'}},
{choamCharity:{turn:2,canceled:false}},
]) assert.equal(supported(Object.assign(structuredClone(charity), patch)), false, JSON.stringify(patch));
const auction = source(); auction.players.push(newPlayer('a','Atreides','atreides'));
auction.auction = {cards:[{id:'lot',name:'Shield',kind:'shield'}], index:0, bid:0,
bidder:null, active:'a', passed:[], opener:0};
auction.response = {kind:'atreidesAuction',owner:'a',passed:[]};
const missingLot = structuredClone(auction); missingLot.auction!.index = 1;
assert.equal(supported(missingLot), false);
const wrongOwner = structuredClone(auction); wrongOwner.response!.owner = 'e';
assert.equal(supported(wrongOwner), false);
const unsupportedLot = structuredClone(auction);
unsupportedLot.richeseAuction = {} as NonNullable<Game['richeseAuction']>;
assert.equal(supported(unsupportedLot), false);
const otherResponse = structuredClone(auction); otherResponse.response!.kind = 'richeseAuction';
assert.equal(supported(otherResponse), false);
const declaration = source(); declaration.richeseCache = [];
declaration.richeseBidding = {owner:'r',event:'round',turn:declaration.turn,stage:'declaration',
position:'first',normalCount:1,normalCountBeforeCache:1,blackMarketSold:false,cacheCanceled:false};
declaration.response = {kind:'richeseAuction',owner:'r',passed:[]};
for (const patch of [
{phase:4}, {richeseCache:undefined}, {auction:auction.auction},
{richeseBidding:{...declaration.richeseBidding,turn:1}},
{richeseBidding:{...declaration.richeseBidding,owner:'e'}},
{richeseBidding:{...declaration.richeseBidding,stage:'lot'}},
{richeseBidding:{...declaration.richeseBidding,position:undefined}},
{richeseBidding:{...declaration.richeseBidding,normalCount:-1}},
{richeseBidding:{...declaration.richeseBidding,normalCountBeforeCache:3}},
{richeseBidding:{...declaration.richeseBidding,cacheCanceled:true}},
{pendingRevival:{}},
]) assert.equal(supported(Object.assign(structuredClone(declaration), patch)), false, JSON.stringify(patch));
const grant = source(); grant.phase = 4; grant.players.push(newPlayer('f','Fremen','fremen'));
grant.players.find(p => p.id === 'f')!.ally = 'r'; grant.players.find(p => p.id === 'r')!.ally = 'f';
grant.freeRevival = [];
grant.response = {kind:'fremenRevivalGrant',owner:'f',recipient:'r',passed:[],
fremenGrant:{turn:grant.turn,owner:'f',recipient:'r'}};
const stale = structuredClone(grant); stale.response!.fremenGrant!.turn--;
assert.equal(supported(stale), false);
const noAlly = structuredClone(grant); noAlly.players.find(p => p.id === 'r')!.ally = null;
assert.equal(supported(noAlly), false);
const wrongRecipient = structuredClone(grant); wrongRecipient.response!.recipient = 'e';
assert.equal(supported(wrongRecipient), false);
const unrelatedChild = structuredClone(grant); unrelatedChild.pendingRevival = {} as NonNullable<Game['pendingRevival']>;
assert.equal(supported(unrelatedChild), false);
});
void test('a legacy table without public Truthtrance history keeps its earlier interruption behavior', () => {
const game = parent('auctionPayment');
delete game.truthHistory;
assert.equal(supported(game), false);
});
void test('profile and nested-interaction boundaries do not admit another parent or module', () => {
const patches: Array<Partial<Game>> = [
{advanced:true}, {status:'setup'}, {phase:6}, {phase:-1}, {turn:0},
{expansions:['choam','unknown']}, {expansions:['ix']},
{response:{kind:'richeseAuction', owner:'r', passed:[]}},
{pendingNullentropy:{} as NonNullable<Game['pendingNullentropy']>},
{pendingTreacheryDiscard:{} as NonNullable<Game['pendingTreacheryDiscard']>},
{pendingShipment:{} as NonNullable<Game['pendingShipment']>},
{pendingKull:{} as NonNullable<Game['pendingKull']>},
{biddingEnd:{} as NonNullable<Game['biddingEnd']>},
{stormResolution:{} as NonNullable<Game['stormResolution']>},
{techTokens:{} as NonNullable<Game['techTokens']>},
{leaderSkills:{} as NonNullable<Game['leaderSkills']>},
{discoveryEnabled:true}, {ecazTreachery:true},
];
for (const patch of patches) {
const game = Object.assign(parent('auctionPayment'), patch), before = structuredClone(game);
assert.equal(supported(game), false, JSON.stringify(patch)); assert.deepEqual(game, before);
}
const wrong = parent('auctionPayment'); wrong.decision = {kind:'revivalStop', player:'e', recipient:'r', revival:'forces'};
assert.equal(supported(wrong), false);
const missing = parent('auctionPayment'); missing.auction = null; assert.equal(supported(missing), false);
const wrongSeat = parent('auctionPayment'); wrongSeat.auction!.bidder = 'r'; assert.equal(supported(wrongSeat), false);
const native = parent('auctionPayment'); native.players.push(newPlayer('i','Ixians','ixians'));
native.expansions = ['choam']; assert.equal(supported(native), false);
const e3 = parent('auctionPayment'); e3.players.push(newPlayer('z','Ecaz','ecaz'));
e3.expansions = ['choam']; assert.equal(supported(e3), false);
e3.expansions = ['choam','ecaz']; assert.equal(supported(e3), true);
});
void test('live current-turn commitments block the interruption; settled and previous-turn records do not', () => {
const game = parent('auctionPayment');
game.shipmentPromises = [{turn:game.turn, player:'e', asker:'r', answer:true, territory:'carthag', minimum:3}];
assert.equal(supported(game), false);
game.shipmentPromises[0].fulfilled = true; assert.equal(supported(game), true);
game.shipmentPromises[0].fulfilled = false; game.shipmentPromises[0].turn--; assert.equal(supported(game), true);
game.shipmentPromises = [];
game.futureBattleLeaderPromises = [{turn:game.turn, asker:'r', player:'e', leader:'leader', answer:true, status:'waiting'}];
assert.equal(supported(game), false);
game.futureBattleLeaderPromises[0].status = 'settled'; assert.equal(supported(game), true);
game.deals = [{status:'active'}] as unknown as Game['deals']; assert.equal(supported(game), false);
});