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
+14
-2
@@ -17,6 +17,18 @@ open mode gates.
|
||||
|
||||
## Current checkpoint and work
|
||||
|
||||
**Upcoming Advanced Charity Truthtrance — 29 October. Prototyped / Partial.**
|
||||
In quiet Advanced Storm and Spice Blow, a Charity answer can bind the player's
|
||||
upcoming ordered turn. Existing early readiness and the Charity queue remain;
|
||||
the private action guide appears only on the recipient's own turn. Genuine
|
||||
Advanced setups exercise Yes/No, a prior Spice Ready, actual claims and bribes,
|
||||
unresolved CHOAM income, controls and four legal bot profiles. The Storm case
|
||||
selects unopened Spice cards. Fifteen related files pass 81 cases, including
|
||||
nine new cases; type checking, lint and production build pass. The forecast
|
||||
and timing are provisional; other preparations, combinations and deployed
|
||||
acceptance remain open.
|
||||
[Scope](TRUTHTRANCE_CHARITY.md#upcoming-advanced-charity--29-october-2026).
|
||||
|
||||
**Advanced ordered Truthtrance Charity — 29 October. Prototyped / Partial.**
|
||||
During live Advanced Charity, an unclaimed player whose turn remains can
|
||||
answer a claim question without changing the original queue. An attainable Yes
|
||||
@@ -27,8 +39,8 @@ bribes, current/future and final turns, four legal bot profiles, and Bene
|
||||
Gesserit's normal and Karama-canceled claim with a conserved Truth-card swap.
|
||||
Thirteen related files pass 72 cases, including 13 new cases; type checking,
|
||||
lint and production build pass. The forecast and timing are provisional;
|
||||
upcoming Advanced windows, broader combinations and deployed acceptance remain
|
||||
open. [Scope](TRUTHTRANCE_CHARITY.md#live-advanced-ordered-charity--29-october-2026).
|
||||
the follow-up above extends quiet upcoming windows; broader combinations and
|
||||
deployed acceptance remain open. [Scope](TRUTHTRANCE_CHARITY.md#live-advanced-ordered-charity--29-october-2026).
|
||||
|
||||
**Basic leaderless preparation with Harkonnen — 29 October. Prototyped /
|
||||
Partial.** Fresh classic Basic games with Harkonnen now use public native
|
||||
|
||||
@@ -1010,11 +1010,16 @@ phase; do not treat an intervening native response as lost eligibility. Use the
|
||||
known own purse and one legal ordinary payment as a bounded future witness,
|
||||
without inventing future spice, CHOAM outcomes or foreign actions. The FAQ
|
||||
supports current-turn action commitments but does not prescribe this forecast.
|
||||
Other preparation paths, combined promises and upcoming Advanced timing remain separate.
|
||||
Other preparation paths and combined promises remain separate; the later
|
||||
Advanced forecast is recorded below.
|
||||
[Source and boundaries](TRUTHTRANCE_CHARITY.md#upcoming-basic-charity--27-october-2026).
|
||||
|
||||
## Truthtrance Charity commitment — 27 October 2026
|
||||
|
||||
The provisional upcoming Charity forecast also covers quiet Advanced Storm
|
||||
and Spice Blow, retaining the original ordered turn and its native continuation.
|
||||
[Scope](TRUTHTRANCE_CHARITY.md#upcoming-advanced-charity--29-october-2026).
|
||||
|
||||
The same provisional claim commitment now includes the live Advanced ordered
|
||||
Charity phase for players with an unclaimed turn remaining. The current queue
|
||||
is preserved, and a private action guide appears only on the respondent's turn.
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# Truthtrance: Charity claim commitment
|
||||
|
||||
## Upcoming Advanced Charity — 29 October 2026
|
||||
|
||||
**Prototyped / Partial.** In a quiet Advanced Storm or Spice Blow window, the
|
||||
existing Charity question can bind a player's upcoming ordered Charity turn.
|
||||
The same detached claim or one-payment forecast uses current known resources
|
||||
without drawing cards, inventing another player's actions or crediting a
|
||||
pending CHOAM opening. Storm and Spice readiness remain intact; no future
|
||||
claim or payment is offered as a live action. The answer carries into the
|
||||
native Charity queue, where the recipient's own turn supplies the existing
|
||||
claim guide and finish restrictions. A pending interruption does not itself
|
||||
release an answer.
|
||||
|
||||
The FAQ supports current-turn action commitments; the forecast and digital
|
||||
ordering are provisional. Upcoming income, additional payment routes, broader
|
||||
combinations and deployed acceptance remain open.
|
||||
|
||||
## Live Advanced ordered Charity — 29 October 2026
|
||||
|
||||
**Prototyped / Partial.** In a quiet live Advanced Charity phase, a player may
|
||||
@@ -12,9 +28,10 @@ the claim until that turn closes. A completed turn cannot be reopened. The
|
||||
existing native claim, payment and cancellation procedures remain authoritative.
|
||||
|
||||
This is a provisional digital interpretation of the FAQ's current-turn
|
||||
commitment, not a Charity-specific publisher ruling. Upcoming Advanced phases,
|
||||
broader payment forecasts, pending responses and other combinations remain
|
||||
open. Native ordered Charity and the original Basic paths remain unchanged.
|
||||
commitment, not a Charity-specific publisher ruling. The follow-up above adds
|
||||
quiet upcoming Advanced windows; broader payment forecasts, pending responses
|
||||
and other combinations remain open. Native ordered Charity and the original
|
||||
Basic paths remain unchanged.
|
||||
|
||||
## Upcoming Basic Charity — 27 October 2026
|
||||
|
||||
|
||||
+3
-3
@@ -17735,8 +17735,8 @@ function charityPromiseRule<T>(run: () => T): T {
|
||||
}
|
||||
}
|
||||
function charityQuestionWindow(g: Game): boolean {
|
||||
const timing = !g.advanced && g.phase >= 0 && g.phase <= 2 && !g.charityOrder ||
|
||||
g.advanced && g.phase === 2 && !!g.charityOrder;
|
||||
const timing = g.phase >= 0 && g.phase < 2 && !g.charityOrder ||
|
||||
g.phase === 2 && (g.advanced ? !!g.charityOrder : !g.charityOrder);
|
||||
return g.status === 'playing' && timing &&
|
||||
!g.response && !g.decision && !g.phaseOpening && !g.pendingKarama && !g.pendingTreacheryDiscard &&
|
||||
(g.phase < 2 || !byFaction(g, 'choam') || g.choamCharity?.turn === g.turn);
|
||||
@@ -17812,7 +17812,7 @@ function reconcileCharityTruth(g: Game,voluntary?: {actor:string;action:Action})
|
||||
const rows: CharityPromise[] = promises.filter(row => row.player === player && !row.fulfilled && !row.released);
|
||||
for (const turn of new Set(rows.map(row => row.turn))) {
|
||||
const open = g.status === 'playing' && g.turn === turn &&
|
||||
(!g.advanced && g.phase >= 0 && g.phase <= 2 ||
|
||||
(g.phase >= 0 && g.phase < 2 || !g.advanced && g.phase === 2 ||
|
||||
g.advanced && g.phase === 2 && !!g.charityOrder?.remaining.includes(player));
|
||||
const before: CharityPromise[] = promises;
|
||||
promises = charityPromiseRule(() => reconcileCharityPromises(before,player,turn,
|
||||
|
||||
+4
-4
@@ -3974,20 +3974,20 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
category: 'Cards',
|
||||
coverage: 'Partial',
|
||||
developmentStage: 'Prototyped',
|
||||
summary: 'Ask whether another player will claim Charity during a supported Basic or live Advanced Charity opportunity.',
|
||||
summary: 'Ask whether another player will claim Charity during a supported Basic or Advanced Charity opportunity.',
|
||||
steps: [
|
||||
'Yes requires an actual native Charity claim while possible; No forbids it through the original Basic Charity phase or the player’s Advanced ordered turn. An answer in concurrent Basic Charity reopens that player’s Ready; earlier Storm or Spice readiness and the Advanced order stay intact, and an earlier claim does not count.',
|
||||
'During Charity a private guide can suggest the exact claim or an ordinary bribe that makes the claim eligible. Before Charity, the forecast offers no executable future action. The bribe is paid now and received at Mentat; the claim uses the normal eligibility, payer and inflation rules.',
|
||||
'A Yes remains binding while a supported claim or payment route exists. A voluntary action cannot abandon it; an external change that removes the supported route releases it. Independent future commitments may require an unknown answer until their joint planning is supported.',
|
||||
'During live Advanced Charity, an unclaimed player whose turn remains can answer without changing the order. Their private guide appears only on their own turn; No ends when that turn closes. Quiet timing and the forecast remain provisional. Upcoming Advanced Charity, other preparations, pending windows and broader combinations remain unfinished.',
|
||||
'In quiet Advanced Storm or Spice Blow, an answer can bind the upcoming Charity turn without changing the current readiness. During live Advanced Charity, an unclaimed player whose turn remains can answer without changing the order. Their private guide appears only on their own turn; No ends when that turn closes. Timing and the forecast remain provisional; other preparations, pending windows and broader combinations remain unfinished.',
|
||||
],
|
||||
related: ['card-truthtrance', 'charity', 'truthtrance-bribe'],
|
||||
checklist: [
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Native Basic upcoming/current and live Advanced ordered claims, one-payment route, turn-bound Yes/No and event-bound guide are connected; wider forecasts and combined obligations remain open.', evidence: ['game/charity-promises.ts', 'game/truthtrance.ts', 'game/engine.ts'] },
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Native Basic and Advanced upcoming/current claims, one-payment route, turn-bound Yes/No and event-bound guide are connected; wider forecasts and combined obligations remain open.', evidence: ['game/charity-promises.ts', 'game/truthtrance.ts', 'game/engine.ts'] },
|
||||
{ area: 'Player controls', status: 'Partial', detail: 'Question, recipient answers, persistent private guidance and claim/Ready reasons are connected; browser acceptance remains open.', evidence: ['components/truthtrance.tsx', 'components/truth-charity-promise.tsx', 'components/charity-claim.tsx', 'components/game-table.tsx'] },
|
||||
{ area: 'AI', status: 'Partial', detail: 'Profiles use the private answer and native guided action; strategy and calibration are deferred.', evidence: ['game/bots.ts', 'tests/truth-charity-commitment-runtime.test.ts'] },
|
||||
{ area: 'Documentation', status: 'Implemented', detail: 'The FAQ source, provisional timing and bounded preparation route are recorded.', evidence: ['docs/TRUTHTRANCE_CHARITY.md', 'docs/RULE_DECISIONS.md'] },
|
||||
{ area: 'Verification', status: 'Partial', detail: 'Focused native and rendered controls cover the Basic and ordered Advanced prototypes; wider combinations and deployed acceptance remain open.', evidence: ['tests/charity-promises.test.ts', 'tests/truth-charity-commitment-runtime.test.ts', 'tests/truth-charity-commitment-controls.test.tsx', 'tests/truth-charity-early-runtime.test.ts', 'tests/truth-charity-early-controls.test.tsx', 'tests/truth-charity-ordered-runtime.test.ts', 'tests/truth-charity-ordered-controls.test.tsx', 'tests/truth-charity-ordered-bg.test.ts'] },
|
||||
{ area: 'Verification', status: 'Partial', detail: 'Focused native and rendered controls cover Basic and Advanced upcoming and ordered prototypes; wider combinations and deployed acceptance remain open.', evidence: ['tests/charity-promises.test.ts', 'tests/truth-charity-commitment-runtime.test.ts', 'tests/truth-charity-commitment-controls.test.tsx', 'tests/truth-charity-early-runtime.test.ts', 'tests/truth-charity-early-controls.test.tsx', 'tests/truth-charity-ordered-runtime.test.ts', 'tests/truth-charity-ordered-controls.test.tsx', 'tests/truth-charity-ordered-bg.test.ts', 'tests/truth-charity-advanced-early.test.ts', 'tests/truth-charity-advanced-early-controls.test.tsx'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
+2
-1
@@ -294,7 +294,8 @@ function parseQuestion(g: Game, asker: string, value: unknown): TruthQuestion {
|
||||
throw new TruthError('Finish this player’s conditional battle answer before binding another action question.');
|
||||
if (v.kind === 'charity') {
|
||||
check(Object.keys(v).sort().join(',') === 'kind,target,turn' && v.turn === g.turn &&
|
||||
g.status === 'playing' && (!g.advanced && g.phase >= 0 && g.phase <= 2 && !g.charityOrder ||
|
||||
g.status === 'playing' && (g.phase >= 0 && g.phase < 2 && !g.charityOrder ||
|
||||
!g.advanced && g.phase === 2 && !g.charityOrder ||
|
||||
g.advanced && g.phase === 2 && !!g.charityOrder?.remaining.includes(v.target as string)) &&
|
||||
g.players.find(player => player.id === v.target)?.charityTurn !== g.turn &&
|
||||
!g.response && !g.decision && !g.phaseOpening && !g.pendingKarama && !g.pendingTreacheryDiscard,
|
||||
|
||||
@@ -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