diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index ca44c567..13bbad1d 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -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 diff --git a/docs/RULE_DECISIONS.md b/docs/RULE_DECISIONS.md index 01a5c7db..ff4537df 100644 --- a/docs/RULE_DECISIONS.md +++ b/docs/RULE_DECISIONS.md @@ -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. diff --git a/docs/TRUTHTRANCE_CHARITY.md b/docs/TRUTHTRANCE_CHARITY.md index 0c8a859c..dd13fa08 100644 --- a/docs/TRUTHTRANCE_CHARITY.md +++ b/docs/TRUTHTRANCE_CHARITY.md @@ -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 diff --git a/game/engine.ts b/game/engine.ts index 3305d7c8..1f50e072 100644 --- a/game/engine.ts +++ b/game/engine.ts @@ -17735,8 +17735,8 @@ function charityPromiseRule(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, diff --git a/game/reference.ts b/game/reference.ts index 7e4a80b2..ed2394dc 100644 --- a/game/reference.ts +++ b/game/reference.ts @@ -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'] }, ], }, { diff --git a/game/truthtrance.ts b/game/truthtrance.ts index 61cc3d3c..a50c9238 100644 --- a/game/truthtrance.ts +++ b/game/truthtrance.ts @@ -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, diff --git a/tests/truth-charity-advanced-early-controls.test.tsx b/tests/truth-charity-advanced-early-controls.test.tsx new file mode 100644 index 00000000..3a2bca37 --- /dev/null +++ b/tests/truth-charity-advanced-early-controls.test.tsx @@ -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):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,/