Files
Dune/tests/kull-whole-turn-scope-controls.test.tsx
T
Quintine 07f1f1f2ae
Verify and publish checkpoint container / container (push) Successful in 10m21s
Support whole-turn answers with Basic Kull
2026-10-29 01:37:33 +11:00

101 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict';
import test from 'node:test';
import { registerHooks } from 'node:module';
import { readFileSync } from 'node:fs';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { handLimit, viewGame, type Action, type Game, type GameView } from '../game/engine';
import { turnActionPromiseModeSupported } from '../game/turn-action-promises';
import { createKullBribeFixture } from './fixture-kull-bribe';
import { act, advance, clean, createKullRicheseSemutaFixture, inventory, player } from './fixture-kull-richese-semuta';
const aliases = registerHooks({
resolve(specifier, context, next) {
return next(specifier === 'next/image' ? 'vinext/shims/image' :
specifier === 'next/link' ? 'vinext/shims/link' : specifier, context);
},
load(url, context, next) {
if (!url.endsWith('.module.css')) return next(url, context);
const css = readFileSync(new URL(url), 'utf8');
const classes = Object.fromEntries([...css.matchAll(/\.([a-zA-Z_][\w-]*)/g)].map(match => [match[1], match[1]]));
return {format:'module', source:`export default ${JSON.stringify(classes)}`, shortCircuit:true};
},
});
const {GameTable} = await import('../components/game-table');
const {Truthtrance} = await import('../components/truthtrance');
const {TurnActionGuideButton} = await import('../components/turn-action-promises');
aliases.deregister();
const table = (game:GameView) => renderToStaticMarkup(createElement(GameTable,
{game,busy:false,send:async () => {},onExit() {}}));
const truth = (game:GameView) => renderToStaticMarkup(createElement(Truthtrance,
{game,busy:false,act() {}}));
void test('Basic printed Kull admits whole-turn questions without opening Advanced or optional modules',()=>{
const classic = {advanced:false, expansions:['choam','ix'], kullPreview:true,
players:[{faction:'choam'},{faction:'emperor'},{faction:'guild'}]};
const richese = {...classic, semutaPreview:true,
players:[...classic.players, {faction:'richese'}]};
assert.equal(turnActionPromiseModeSupported(classic),true);
assert.equal(turnActionPromiseModeSupported(richese),true);
assert.equal(turnActionPromiseModeSupported({...richese,kullPreview:false}),true);
assert.equal(turnActionPromiseModeSupported({...richese,advanced:true}),false);
assert.equal(turnActionPromiseModeSupported({...classic,expansions:['choam']}),false);
assert.equal(turnActionPromiseModeSupported({...richese,players:[...richese.players,{faction:'richese'}]}),false);
for (const flag of ['nexusKullPreview','richeseBetrayalPreview','nexusCards','homeworlds',
'leaderSkills','techTokens','strongholdCards','discoveryEnabled','ecazTreachery'] as const)
assert.equal(turnActionPromiseModeSupported({...richese,[flag]:true}),false,flag);
});
/** Both sources use genuine Basic setup and phases. In the Richese case only
* one original physical Truthtrance is moved to Harkonnen's hand; the native
* Richese cache purchase and all subsequent actions remain real. */
function source(richese:boolean): {game:Game; asker:string; card:string; originalCards:string[]} {
if (!richese) {
const f = createKullBribeFixture();
return {game:structuredClone(f.game),asker:f.asker,card:f.truth,originalCards:f.originalCards};
}
const f = createKullRicheseSemutaFixture();
const game = structuredClone(f.game);
const zones = [game.deck,game.discard,...(game.richeseCache?[game.richeseCache]:[]),
...(game.richeseRemoved?[game.richeseRemoved]:[]),...game.players.map(seat=>seat.hand)];
const zone=zones.find(cards=>cards.some(card=>card.effect==='truthtrance')); assert.ok(zone);
const [card]=zone.splice(zone.findIndex(row=>row.effect==='truthtrance'),1);
const asker=player(game,'harkonnen'); assert.ok(asker.hand.length < handLimit(asker)); asker.hand.push(card);
assert.deepEqual(inventory(game),f.originalCards);
return {game,asker:'harkonnen',card:card.id,originalCards:f.originalCards};
}
for (const richese of [false,true]) void test(`Basic Kull ${richese?'with':'without'} Richese renders and executes a whole-answer guide`,()=>{
const f=source(richese); let game=act(f.game,f.asker,{type:'card',card:f.card});
while (game.truthtrance?.stage==='priority') {
const seat=game.players.find(row=>!game.truthtrance!.passed.includes(row.id)); assert.ok(seat);
game=act(game,seat.id,{type:'truthPass'});
}
const asker=viewGame(game,f.asker);
assert.ok(asker.truthTurnActionTargets.includes('emperor'));
assert.match(truth(asker),/Combine printed card plays, native battle powers and this turn’s other actions/);
assert.match(table(asker),/Combine printed card plays, native battle powers and this turn’s other actions/);
game=act(game,f.asker,{type:'truthAsk',question:{kind:'turnActions',target:'emperor',turn:game.turn,
mixed:{kind:'and',terms:[
{kind:'action',action:{kind:'payment',recipient:'choam',minimum:1}},
{kind:'action',action:{kind:'ship',territory:'polar_sink',minimum:1}},
]}}});
const respondent=viewGame(game,'emperor');
assert.ok(respondent.truthTurnActionAnswers?.includes('yes'));
assert.ok(respondent.truthTurnActionAnswers?.includes('no'));
assert.equal(viewGame(game,f.asker).truthTurnActionAnswers,null);
assert.match(truth(respondent),/Answer Yes/); assert.match(truth(respondent),/Answer No/);
assert.match(table(respondent),/Answer Yes/); assert.match(table(respondent),/Answer No/);
game=act(game,'emperor',{type:'truthAnswer',answer:'yes'});
game=advance(game,clean);
const own=viewGame(game,'emperor'), sent:Action[]=[]; assert.ok(own.turnActionGuide);
assert.match(table(own),/while honoring your whole answer/);
const button=TurnActionGuideButton({game:own,busy:false,act(action) {sent.push(action);}});
assert.ok(button); assert.equal(button.props.disabled,false); button.props.onClick();
assert.deepEqual(sent,[own.turnActionGuide.action]);
game=act(game,'emperor',sent[0]);
assert.ok(game.turnActionPromises?.some(row=>row.player==='emperor' && !row.released));
assert.deepEqual(inventory(game),f.originalCards);
});