Support Truthtrance in ordered Charity
Verify and publish checkpoint container / container (push) Canceled after 0s
Verify and publish checkpoint container / container (push) Canceled after 0s
This commit is contained in:
1 parent
31c6e79c80
commit
68c7434a20
11 files changed
+425
-19
No files matched your search
+21
-8
@@ -17725,7 +17725,7 @@ function phaseBidWitnesses(g: Game, p: Player): PhaseBidOpportunity[] {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/** The first structured Charity question covers the live concurrent Basic phase.
|
||||
/** A structured Charity answer retains the recipient's native opportunity.
|
||||
* A detached native action quotes the real entitlement and payer; it never
|
||||
* commits a payment or opens a response at the table. */
|
||||
function charityPromiseRule<T>(run: () => T): T {
|
||||
@@ -17735,17 +17735,25 @@ function charityPromiseRule<T>(run: () => T): T {
|
||||
}
|
||||
}
|
||||
function charityQuestionWindow(g: Game): boolean {
|
||||
return g.status === 'playing' && !g.advanced && g.phase >= 0 && g.phase <= 2 && !g.charityOrder &&
|
||||
const timing = !g.advanced && g.phase >= 0 && g.phase <= 2 && !g.charityOrder ||
|
||||
g.advanced && g.phase === 2 && !!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);
|
||||
}
|
||||
function charityQuestionTarget(g: Game, player: Player): boolean {
|
||||
return player.charityTurn !== g.turn && (!g.charityOrder || g.charityOrder.remaining.includes(player.id));
|
||||
}
|
||||
function charityPromiseCompletion(g: Game, player: string): Action | null {
|
||||
if (!charityQuestionWindow(g) || getPlayer(g,player).charityTurn === g.turn) return null;
|
||||
if (!charityQuestionWindow(g) || !charityQuestionTarget(g,getPlayer(g,player))) return null;
|
||||
// In early phases the action is only a detached witness for the later
|
||||
// Charity opportunity. Never project it as a currently executable guide.
|
||||
const trial = g.phase < 2 ? charityForecastContext(g) : structuredClone(g);
|
||||
trial.truthtrance = null;
|
||||
trial.ready = trial.ready.filter(id => id !== player);
|
||||
// Quote this seat's eventual native claim without changing the live order.
|
||||
// The caller has already checked that its opportunity remains in the queue.
|
||||
if (trial.charityOrder) delete trial.charityOrder;
|
||||
// Either CHOAM opening outcome pays another faction's ordinary claim; use
|
||||
// the existing bank-fallback branch only on this detached quote. Its own
|
||||
// future opening income is uncertain and is handled separately below.
|
||||
@@ -17778,7 +17786,7 @@ function charityPromiseCanClaim(g: Game, player: string): boolean {
|
||||
return !!charityPromiseCompletion(g,player);
|
||||
}
|
||||
function charityTruthAnswers(g: Game, p: Player, turn: number): TruthAnswer[] {
|
||||
if (!charityQuestionWindow(g) || turn !== g.turn || p.charityTurn === turn) return [];
|
||||
if (!charityQuestionWindow(g) || turn !== g.turn || !charityQuestionTarget(g,p)) return [];
|
||||
if (richeseBidOtherPromises(g,p)) return ['unknown'];
|
||||
// CHOAM's own unsettled opening changes its purse and depends on another
|
||||
// player's cancellation. No remains an owned choice; do not invent Yes.
|
||||
@@ -17792,7 +17800,7 @@ function bindCharityTruth(g: Game,p: Player,turn: number,answer: boolean,asker:
|
||||
g.charityPromises = charityPromiseRule(() => bindCharityPromise(g.charityPromises ?? [],p.id,asker,turn,answer,charityPromiseCanClaim(g,p.id)));
|
||||
// Only the actual concurrent Charity phase reopens Ready; Storm/Spice
|
||||
// readiness belongs to the original phase and must remain intact.
|
||||
if (g.phase === 2) g.ready = g.ready.filter(id => id !== p.id);
|
||||
if (g.phase === 2 && !g.charityOrder) g.ready = g.ready.filter(id => id !== p.id);
|
||||
}
|
||||
function reconcileCharityTruth(g: Game,voluntary?: {actor:string;action:Action}): void {
|
||||
if (!g.charityPromises?.length || g.truthtrance || g.response || g.decision || g.phaseOpening ||
|
||||
@@ -17803,7 +17811,9 @@ function reconcileCharityTruth(g: Game,voluntary?: {actor:string;action:Action})
|
||||
for (const player of people) {
|
||||
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.advanced && g.phase >= 0 && g.phase <= 2 && g.turn === turn;
|
||||
const open = g.status === 'playing' && g.turn === turn &&
|
||||
(!g.advanced && g.phase >= 0 && g.phase <= 2 ||
|
||||
g.advanced && g.phase === 2 && !!g.charityOrder?.remaining.includes(player));
|
||||
const before: CharityPromise[] = promises;
|
||||
promises = charityPromiseRule(() => reconcileCharityPromises(before,player,turn,
|
||||
{phaseOpen:open,canClaim:open && charityPromiseCanClaim(g,player),
|
||||
@@ -17815,7 +17825,8 @@ function reconcileCharityTruth(g: Game,voluntary?: {actor:string;action:Action})
|
||||
g.charityPromises = promises;
|
||||
}
|
||||
function projectedCharityTruthGuide(g: Game,player: string): {action:Action} | null {
|
||||
if (g.phase !== 2 || g.truthtrance || !charityQuestionWindow(g) || g.ready.includes(player)) return null;
|
||||
if (g.phase !== 2 || g.truthtrance || !charityQuestionWindow(g) || g.ready.includes(player) ||
|
||||
g.charityOrder && g.charityOrder.remaining[0] !== player) return null;
|
||||
const promise = liveCharityPromises(g.charityPromises ?? [],player,g.turn).find(row => row.answer);
|
||||
const action = promise && charityPromiseCompletion(g,player);
|
||||
return action && promise ? {action:{...action,charityPromiseEvent:promise.event,charityPromiseTurn:promise.turn}} : null;
|
||||
@@ -30385,11 +30396,13 @@ function guardCharityOrderFinish(g: Game, player: string, frame: CharityOrder, a
|
||||
closed.ready.push(player);
|
||||
closed.active = frame.remaining[0] ?? null;
|
||||
const voluntary = { actor: player, action };
|
||||
reconcileCharityTruth(closed, voluntary);
|
||||
reconcileTravelPromises(closed, player, action, g.phase, g);
|
||||
reconcileRevivalPromises(closed, voluntary);
|
||||
reconcileBribePromises(closed, voluntary);
|
||||
reconcileShipmentPromises(closed, voluntary);
|
||||
reconcileGroundMovementPromises(closed, player, action, g.phase);
|
||||
if (closed.charityPromises) g.charityPromises = closed.charityPromises;
|
||||
}
|
||||
function revivalOrderRule<T>(calculate: () => T): T {
|
||||
try { return calculate(); }
|
||||
@@ -48138,7 +48151,7 @@ export function viewGame(state: Game, id: string, now?: number) {
|
||||
}).map(player => player.id) : [],
|
||||
truthBidAnswers: g.truthtrance?.stage === 'answer' && g.truthtrance.question?.target === id &&
|
||||
g.truthtrance.question.kind === 'bid' ? bidTruthAnswers(g, me, g.truthtrance.question) : null,
|
||||
truthCharityTargets: charityQuestionWindow(g) ? g.players.filter(player => player.charityTurn !== g.turn).map(player => player.id) : [],
|
||||
truthCharityTargets: charityQuestionWindow(g) ? g.players.filter(player => charityQuestionTarget(g,player)).map(player => player.id) : [],
|
||||
truthCharityAnswers: g.truthtrance?.stage === 'answer' && g.truthtrance.question?.target === id &&
|
||||
g.truthtrance.question.kind === 'charity' ? charityTruthAnswers(g,me,g.truthtrance.question.turn) : null,
|
||||
truthCharityPromises: (g.charityPromises ?? []).filter(row => row.player === id).map(row => ({...row})),
|
||||
|
||||
+5
-5
@@ -3974,20 +3974,20 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
category: 'Cards',
|
||||
coverage: 'Partial',
|
||||
developmentStage: 'Prototyped',
|
||||
summary: 'Ask whether another player will claim Charity after answering during this turn’s upcoming or current Basic Charity phase.',
|
||||
summary: 'Ask whether another player will claim Charity during a supported Basic or live Advanced Charity opportunity.',
|
||||
steps: [
|
||||
'Yes requires an actual native Charity claim while possible; No forbids it until the original Charity phase ends. An answer in Charity reopens that player’s concurrent Ready; earlier Storm or Spice readiness stays intact, and an earlier claim does not count.',
|
||||
'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.',
|
||||
'The supported quiet Basic Storm, Spice and Charity timing is provisional. Advanced ordered Charity, other preparations, pending windows and broader combinations remain unfinished.',
|
||||
'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.',
|
||||
],
|
||||
related: ['card-truthtrance', 'charity', 'truthtrance-bribe'],
|
||||
checklist: [
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Native Basic upcoming/current claim, one-payment route, turn-bound Yes/No and event-bound live 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 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: '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 prototype; 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'] },
|
||||
{ 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'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
+4
-2
@@ -294,9 +294,11 @@ 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.advanced && g.phase >= 0 && 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,
|
||||
'Choose this turn’s upcoming or current Basic Charity in a quiet window.');
|
||||
'Choose an unclaimed Charity opportunity in a quiet supported window.');
|
||||
return { kind: 'charity', target: v.target as string, turn: g.turn };
|
||||
}
|
||||
if (v.kind === 'mixedRevival') {
|
||||
|
||||
Reference in new issue
Block a user