Support early Advanced Charity commitments
Verify and publish checkpoint container / container (push) Successful in 11m25s
Verify and publish checkpoint container / container (push) Successful in 11m25s
This commit is contained in:
1 parent
68c7434a20
commit
ad0abfef98
8 files changed
+294
-14
No files matched your search
@@ -0,0 +1,76 @@
|
||||
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 { viewGame, type Game } from '../game/engine';
|
||||
import { createTruthTurnAmalFixture } from './fixture-truth-turn-amal';
|
||||
import { actTruthCharityPayments as act, advanceTruthCharityPayments as advance,
|
||||
createTruthCharityPaymentsFixture, truthCharityPaymentsClean as clean,
|
||||
truthCharityPaymentsInventory as inventory } from './fixture-truth-charity-payments';
|
||||
|
||||
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 {Truthtrance} = await import('../components/truthtrance');
|
||||
const {TruthCharityPromise} = await import('../components/truth-charity-promise');
|
||||
aliases.deregister();
|
||||
|
||||
/** Actual Advanced setup and original Truth cards; phase 1 also plays Amal.
|
||||
* The native fixture selects only unopened starting cards and Spice order. */
|
||||
function fixture(phase:0|1) {
|
||||
const base = phase === 0 ? createTruthTurnAmalFixture({phase:0,advanced:true})
|
||||
: createTruthCharityPaymentsFixture({source:'afterAmal',advanced:true});
|
||||
const game = phase === 0
|
||||
? advance(structuredClone(base.game), state => state.phase === 0 && clean(state), [], true)
|
||||
: structuredClone(base.game);
|
||||
assert.equal(game.phase,phase); assert.equal(game.advanced,true); assert.ok(clean(game));
|
||||
assert.deepEqual(inventory(game),base.originalCards);
|
||||
return {...base,game};
|
||||
}
|
||||
function declare(f:ReturnType<typeof fixture>):Game {
|
||||
let game = act(f.game,f.asker,{type:'card',card:f.truths[0]});
|
||||
while (game.truthtrance?.stage === 'priority') {
|
||||
const actor = game.players.find(player => !game.truthtrance!.passed.includes(player.id)); assert.ok(actor);
|
||||
game = act(game,actor.id,{type:'truthPass'});
|
||||
}
|
||||
assert.equal(game.truthtrance?.stage,'ask'); return game;
|
||||
}
|
||||
|
||||
for (const phase of [0,1] as const) void test(
|
||||
`Advanced phase ${phase} presents the upcoming Charity question and private guidance`, () => {
|
||||
const f = fixture(phase), declared = declare(f), asker = viewGame(declared,f.asker);
|
||||
const questionMarkup = renderToStaticMarkup(createElement(Truthtrance,{game:asker,busy:false,act() {}}));
|
||||
assert.ok(asker.truthCharityTargets.includes(f.target));
|
||||
assert.match(questionMarkup,/<option value="charity">[^<]*[Cc]harity/);
|
||||
const asked = act(declared,f.asker,{type:'truthAsk',question:{kind:'charity',target:f.target,turn:declared.turn}});
|
||||
const target = viewGame(asked,f.target);
|
||||
assert.ok(target.truthCharityAnswers?.includes('yes'));
|
||||
assert.ok(target.truthCharityAnswers?.includes('no'));
|
||||
assert.equal(viewGame(asked,f.asker).truthCharityAnswers,null);
|
||||
assert.equal(viewGame(asked,f.observer).truthCharityAnswers,null);
|
||||
const bound = act(asked,f.target,{type:'truthAnswer',answer:'yes'});
|
||||
const own = viewGame(bound,f.target), observer = viewGame(bound,f.observer);
|
||||
assert.deepEqual(bound.ready,asked.ready);
|
||||
assert.equal(own.truthCharityGuide,null);
|
||||
assert.ok(own.truthCharityPromises.some(promise => promise.answer && !promise.fulfilled && !promise.released));
|
||||
const markup = renderToStaticMarkup(createElement(TruthCharityPromise,
|
||||
{game:own,busy:false,send() { assert.fail('Rendering must not send.'); }}));
|
||||
assert.match(markup,/upcoming Charity phase/);
|
||||
assert.match(markup,/Charity has not opened yet/);
|
||||
assert.doesNotMatch(markup,/data-testid="charity-promise-guide"/);
|
||||
assert.equal(observer.truthCharityPromises.length,0);
|
||||
assert.equal(observer.truthCharityGuide,null);
|
||||
assert.deepEqual(inventory(bound),f.originalCards);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,169 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { viewGame, type Action, type Game } from '../game/engine';
|
||||
import { beforeIxSpiceDraw, orderIxSpiceCards } from './fixture-ix-spice-cards';
|
||||
import { createTruthTurnAmalFixture } from './fixture-truth-turn-amal';
|
||||
import { actTruthCharityPayments as act, advanceTruthCharityPayments as advance,
|
||||
createTruthCharityPaymentsFixture, truthCharityPaymentsClean as clean,
|
||||
truthCharityPaymentsInventory as inventory, truthCharityPaymentsPlayer as player } from './fixture-truth-charity-payments';
|
||||
import { actTruthtranceChoamPromises as actChoam,
|
||||
createTruthtranceChoamPromisesFixture as choamFixture,
|
||||
truthtranceChoamPromisesInventory as choamInventory,
|
||||
truthtranceChoamPromisesPlayer as choamPlayer } from './fixture-truthtrance-choam-promises';
|
||||
|
||||
/** Genuine Advanced selected-Ix public start and original starting cards. Storm
|
||||
* uses a quiet opening after real confirmations; Spice uses the actual Amal
|
||||
* and unopened native Spice ordering. The Storm continuation selects a safe
|
||||
* unopened Spice prefix. No wallet, readiness, promise or phase is installed. */
|
||||
function fixture(phase:0|1) {
|
||||
const base = phase === 0 ? createTruthTurnAmalFixture({phase:0,advanced:true})
|
||||
: createTruthCharityPaymentsFixture({source:'afterAmal',advanced:true});
|
||||
const game = phase === 0
|
||||
? advance(structuredClone(base.game), state => state.phase === 0 && clean(state), [], true)
|
||||
: structuredClone(base.game);
|
||||
assert.equal(game.phase,phase); assert.equal(game.advanced,true); assert.ok(clean(game));
|
||||
assert.equal(player(game,base.target).charityTurn,undefined);
|
||||
assert.deepEqual(inventory(game),base.originalCards);
|
||||
return {...base,game,initialPhase:phase,
|
||||
charityQuestion:{kind:'charity' as const,target:base.target,turn:game.turn}};
|
||||
}
|
||||
function ask(f:ReturnType<typeof fixture>, source = f.game):Game {
|
||||
let game = act(source,f.asker,{type:'card',card:f.truths[0]});
|
||||
while (game.truthtrance?.stage === 'priority') {
|
||||
const next = game.players.find(row => !game.truthtrance!.passed.includes(row.id)); assert.ok(next);
|
||||
game = act(game,next.id,{type:'truthPass'});
|
||||
}
|
||||
assert.equal(game.truthtrance?.stage,'ask');
|
||||
return act(game,f.asker,{type:'truthAsk',question:f.charityQuestion});
|
||||
}
|
||||
function bind(f:ReturnType<typeof fixture>, answer:'yes'|'no', source = f.game) {
|
||||
const asked = ask(f,source);
|
||||
assert.ok(viewGame(asked,f.target).truthCharityAnswers?.includes(answer));
|
||||
return {asked, game:act(asked,f.target,{type:'truthAnswer',answer})};
|
||||
}
|
||||
|
||||
void test('an early Advanced Charity answer preserves a real prior Spice Blow Ready', () => {
|
||||
const f=fixture(1);
|
||||
const blow=advance(f.game,state => !!state.spiceWindow && clean(state),[],true);
|
||||
const ready=act(blow,f.target,{type:'ready'});
|
||||
assert.ok(ready.ready.includes(f.target));
|
||||
const {asked,game:bound}=bind(f,'yes',ready);
|
||||
assert.ok(asked.ready.includes(f.target));
|
||||
assert.deepEqual(bound.ready,asked.ready);
|
||||
let game=reachCharity(bound);
|
||||
assert.ok(game.charityOrder);
|
||||
if (game.charityOrder.remaining[0]!==f.target) assert.equal(viewGame(game,f.target).truthCharityGuide,null);
|
||||
game=ownTurn(game,f.target);
|
||||
assert.ok(viewGame(game,f.target).truthCharityGuide);
|
||||
assert.deepEqual(inventory(game),f.originalCards);
|
||||
});
|
||||
function reachCharity(source:Game):Game {
|
||||
let game = source;
|
||||
if (game.phase === 0) {
|
||||
game = advance(game,beforeIxSpiceDraw,[],true);
|
||||
orderIxSpiceCards(game,[{territory:'hagga_basin'},{territory:'broken_land'}]);
|
||||
}
|
||||
return advance(game,state => state.phase === 2 && !!state.charityOrder && clean(state),[],true);
|
||||
}
|
||||
function ownTurn(source:Game,target:string):Game {
|
||||
let game=source;
|
||||
for (let count=0; game.phase===2 && game.charityOrder?.remaining[0]!==target && count<6; count++) {
|
||||
const actor=game.charityOrder?.remaining[0]; assert.ok(actor);
|
||||
game=act(game,actor,{type:'ready'});
|
||||
}
|
||||
assert.equal(game.phase,2); assert.equal(game.charityOrder?.remaining[0],target);
|
||||
return game;
|
||||
}
|
||||
function reject(game:Game,actor:string,action:Action) {
|
||||
const before=structuredClone(game);
|
||||
assert.throws(() => act(game,actor,action)); assert.deepEqual(game,before);
|
||||
}
|
||||
|
||||
for (const phase of [0,1] as const) {
|
||||
void test(`Advanced phase ${phase} Charity Yes survives openings and the ordered queue`, () => {
|
||||
const f=fixture(phase), {asked,game:bound}=bind(f,'yes');
|
||||
assert.deepEqual(bound.ready,asked.ready);
|
||||
assert.equal(viewGame(bound,f.target).truthCharityGuide,null);
|
||||
assert.ok(viewGame(bound,f.target).truthCharityPromises.some(row => row.answer && !row.fulfilled && !row.released));
|
||||
assert.equal(bound.discard.filter(card => card.id===f.truths[0]).length,1);
|
||||
let game=reachCharity(bound);
|
||||
assert.equal(game.phase,2); assert.ok(game.charityOrder);
|
||||
assert.ok(viewGame(game,f.target).truthCharityPromises.some(row => row.answer && !row.fulfilled && !row.released));
|
||||
if (game.charityOrder.remaining[0]!==f.target) assert.equal(viewGame(game,f.target).truthCharityGuide,null);
|
||||
game=ownTurn(game,f.target);
|
||||
reject(game,f.target,{type:'ready'});
|
||||
let guide=viewGame(game,f.target).truthCharityGuide; assert.ok(guide);
|
||||
game=act(game,f.target,guide.action);
|
||||
if (guide.action.type==='bribe') {
|
||||
guide=viewGame(game,f.target).truthCharityGuide; assert.ok(guide);
|
||||
assert.equal(guide.action.type,'charity'); game=act(game,f.target,guide.action);
|
||||
}
|
||||
assert.equal(player(game,f.target).charityTurn,game.turn);
|
||||
assert.ok(viewGame(game,f.target).truthCharityPromises.some(row => row.answer && row.fulfilled));
|
||||
assert.deepEqual(inventory(game),f.originalCards);
|
||||
});
|
||||
|
||||
void test(`Advanced phase ${phase} Charity No remains until the respondent's ordered turn closes`, () => {
|
||||
const f=fixture(phase), {asked,game:bound}=bind(f,'no');
|
||||
assert.deepEqual(bound.ready,asked.ready);
|
||||
assert.equal(viewGame(bound,f.target).truthCharityGuide,null);
|
||||
let game=ownTurn(reachCharity(bound),f.target);
|
||||
assert.ok(viewGame(game,f.target).truthCharityPromises.some(row => !row.answer && !row.fulfilled && !row.released));
|
||||
const amount=player(game,f.target).spice-1;
|
||||
if (amount>0) game=act(game,f.target,{type:'bribe',target:f.asker,amount});
|
||||
assert.ok(player(game,f.target).spice<2);
|
||||
reject(game,f.target,{type:'charity'});
|
||||
game=act(game,f.target,{type:'ready'});
|
||||
assert.ok(game.phase!==2 || !game.charityOrder?.remaining.includes(f.target));
|
||||
assert.ok(viewGame(game,f.target).truthCharityPromises.some(row => !row.answer && row.fulfilled));
|
||||
assert.deepEqual(inventory(game),f.originalCards);
|
||||
});
|
||||
}
|
||||
|
||||
void test('all four bots can answer before Advanced Charity and use the later legal guide', () => {
|
||||
const f=fixture(1), asked=ask(f);
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const response=viewGame(asked,f.target);
|
||||
response.players.find(row => row.id===f.target)!.bot=difficulty;
|
||||
const answer=botActions(response)[0]; assert.ok(answer);
|
||||
assert.equal(answer.type,'truthAnswer');
|
||||
assert.ok(response.truthCharityAnswers?.includes(answer.answer as 'yes'|'no'));
|
||||
let game=act(structuredClone(asked),f.target,{type:'truthAnswer',answer:'yes'});
|
||||
assert.equal(viewGame(game,f.target).truthCharityGuide,null);
|
||||
game=ownTurn(reachCharity(game),f.target);
|
||||
const own=viewGame(game,f.target); own.players.find(row => row.id===f.target)!.bot=difficulty;
|
||||
const guide=own.truthCharityGuide; assert.ok(guide);
|
||||
assert.deepEqual(botActions(own)[0],guide.action);
|
||||
game=act(game,f.target,guide.action);
|
||||
const claim=viewGame(game,f.target).truthCharityGuide;
|
||||
if (claim) game=act(game,f.target,claim.action);
|
||||
assert.equal(player(game,f.target).charityTurn,game.turn);
|
||||
assert.deepEqual(inventory(game),f.originalCards);
|
||||
}
|
||||
});
|
||||
|
||||
void test('an early Advanced CHOAM question does not invent unresolved opening income', () => {
|
||||
const f=choamFixture({advanced:true}), source=structuredClone(f.game);
|
||||
assert.equal(source.phase,0); assert.equal(source.choamCharity,undefined);
|
||||
assert.equal(choamPlayer(source,f.target).spice,f.rawCash);
|
||||
let game=actChoam(source,f.asker,{type:'card',card:f.truth});
|
||||
while (game.truthtrance?.stage==='priority') {
|
||||
const actor=game.players.find(row => !game.truthtrance!.passed.includes(row.id)); assert.ok(actor);
|
||||
game=actChoam(game,actor.id,{type:'truthPass'});
|
||||
}
|
||||
game=actChoam(game,f.asker,{type:'truthAsk',question:{kind:'charity',target:f.target,turn:game.turn}});
|
||||
const own=viewGame(game,f.target);
|
||||
assert.ok(own.truthCharityAnswers?.includes('no'));
|
||||
assert.ok(own.truthCharityAnswers?.includes('unknown'));
|
||||
assert.ok(!own.truthCharityAnswers?.includes('yes'));
|
||||
const before=structuredClone(game);
|
||||
assert.throws(() => actChoam(game,f.target,{type:'truthAnswer',answer:'yes'}));
|
||||
assert.deepEqual(game,before);
|
||||
game=actChoam(game,f.target,{type:'truthAnswer',answer:'no'});
|
||||
assert.equal(choamPlayer(game,f.target).spice,f.rawCash);
|
||||
assert.equal(game.choamCharity,undefined);
|
||||
assert.ok(viewGame(game,f.target).truthCharityPromises.some(row => !row.answer && !row.fulfilled && !row.released));
|
||||
assert.deepEqual(choamInventory(game),f.originalInventory);
|
||||
});
|
||||
Reference in new issue
Block a user