Settle CHOAM charity claims after depleted funding
Keep CHOAM's own poverty claim bank-funded. Prototype full other-faction claims using available CHOAM cash and an explicitly provisional bank residual; retain original eligibility, cancellation, Homeworld portions and claim limits. Connect persistent controls and genuine public setup/bribe evidence. Complete rules and deployed acceptance remain open.
This commit is contained in:
1 parent
52832dc0fd
commit
d63efccb55
15 files changed
+798
-58
No files matched your search
+21
-1
@@ -38,8 +38,28 @@ export function charityAmount(
|
||||
|
||||
export function charityPayer(
|
||||
g: Pick<Game, 'players' | 'turn' | 'choamCharity'>,
|
||||
beneficiary?: string,
|
||||
) {
|
||||
return g.choamCharity?.turn === g.turn && !g.choamCharity.canceled
|
||||
? g.players.find((p) => p.faction === 'choam')
|
||||
? g.players.find((p) => p.faction === 'choam' && p.id !== beneficiary)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export class CharityFundingError extends Error {}
|
||||
|
||||
/** Preview policy: preserve the claimant's full entitlement without negative
|
||||
* cash. Only the ordinary portion can debit another faction's CHOAM wallet;
|
||||
* the bank covers any shortfall and every Homeworld addition. No loan is made.
|
||||
* Keep this private calculation out of rival projections and public logs. */
|
||||
export function quoteCharityFunding(
|
||||
amounts: { ordinary: number; homeworld: number },
|
||||
payerSpice: number | null,
|
||||
): { choam: number; bank: number } {
|
||||
const { ordinary, homeworld } = amounts;
|
||||
if (![ordinary, homeworld].every(n => Number.isSafeInteger(n) && n >= 0) ||
|
||||
!Number.isSafeInteger(ordinary + homeworld) ||
|
||||
(payerSpice !== null && (!Number.isSafeInteger(payerSpice) || payerSpice < 0)))
|
||||
throw new CharityFundingError('The charity funding amounts are invalid.');
|
||||
const choam = payerSpice === null ? 0 : Math.min(ordinary, payerSpice);
|
||||
return { choam, bank: ordinary - choam + homeworld };
|
||||
}
|
||||
+13
-11
@@ -536,6 +536,8 @@ import {
|
||||
charityAmount,
|
||||
charityQuote,
|
||||
charityPayer,
|
||||
quoteCharityFunding,
|
||||
CharityFundingError,
|
||||
charityMultiplier,
|
||||
type Inflation,
|
||||
} from './charity';
|
||||
@@ -21336,20 +21338,20 @@ function payCharity(g: Game, player: Player, amount: number, homeworld = 0) {
|
||||
'The Homeworld charity payment is invalid.',
|
||||
);
|
||||
const ordinary = amount - homeworld;
|
||||
const payer = charityPayer(g);
|
||||
// The published charity rule does not specify insolvency after intervening
|
||||
// spending. Fail atomically while this edge case remains under audit; never
|
||||
// mint a bank subsidy or create a negative spice balance silently.
|
||||
requireRule(
|
||||
!payer || payer.spice >= ordinary,
|
||||
'CHOAM cannot fund this charity claim.',
|
||||
);
|
||||
if (payer) payer.spice -= ordinary;
|
||||
const payer = charityPayer(g, player.id);
|
||||
let funding: ReturnType<typeof quoteCharityFunding>;
|
||||
try {
|
||||
funding = quoteCharityFunding({ ordinary, homeworld }, payer?.spice ?? null);
|
||||
} catch (error) {
|
||||
if (error instanceof CharityFundingError) throw new RuleError(error.message);
|
||||
throw error;
|
||||
}
|
||||
if (payer) payer.spice -= funding.choam;
|
||||
player.spice += amount;
|
||||
if (homeworld)
|
||||
log(
|
||||
g,
|
||||
`${player.name} received ${ordinary} ordinary charity spice ${payer ? `from ${payer.name}` : 'from the Spice Bank'} and ${homeworld} additional bank spice for low Homeworld population.`,
|
||||
`${player.name} received ${ordinary} ordinary charity spice ${payer ? `under ${payer.name}’s provisional funding rule` : 'from the Spice Bank'} and ${homeworld} additional bank spice for low Homeworld population.`,
|
||||
{ faction: player.faction, name: 'Homeworld charity' },
|
||||
);
|
||||
techIncome(g, 'production', player);
|
||||
@@ -36737,7 +36739,7 @@ export function viewGame(state: Game, id: string) {
|
||||
...charityQuote(g, me),
|
||||
multiplier: charityMultiplier(g),
|
||||
amount: charityAmount(g, me),
|
||||
payer: charityPayer(g)?.id ?? null,
|
||||
payer: charityPayer(g, me.id)?.id ?? null,
|
||||
incomePending: !!byFaction(g, 'choam') && g.choamCharity?.turn !== g.turn,
|
||||
incomeCanceled:
|
||||
g.choamCharity?.turn === g.turn && g.choamCharity.canceled,
|
||||
|
||||
+57
-3
@@ -4080,10 +4080,10 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'CHOAM collects before charity claims and holds up to five treachery cards.',
|
||||
steps: [
|
||||
'At the start of Charity, resolve CHOAM’s public Karama response. If allowed, CHOAM receives two spice per faction, including itself. In a six-player game this is twelve spice.',
|
||||
'Other factions claim normally, with payments taken from CHOAM’s spice. Advanced Bene Gesserit receives two even when wealthy; its cancelable claim resolves before CHOAM pays.',
|
||||
'Canceling CHOAM’s income sends all charity payments that turn to the bank. CHOAM can then claim ordinary charity if it has zero or one spice. Cancellation does not affect next turn’s income.',
|
||||
'Other factions claim normally from CHOAM. Under an explicit provisional shortfall policy, CHOAM pays the available ordinary contribution and the Spice Bank supplies any remainder, preserving the full eligible claim. This unresolved funding choice can shift costs to the bank after voluntary transfers. Advanced Bene Gesserit receives two even when wealthy; its cancelable claim resolves before any payment.',
|
||||
'Canceling CHOAM’s income sends all charity payments that turn to the bank. CHOAM’s own ordinary poverty claim also uses the bank, even after allowed opening income, because its faction payer rule names other factions. Every claimant still has one eligible claim per turn. Cancellation does not affect next turn’s income.',
|
||||
'CHOAM may hold five treachery cards and can bid for a fifth. A full five-card hand prevents further purchases, including purchases with Karama.',
|
||||
'Inflation now has engine support; see its topic for the two-turn lifecycle. Insolvency after intervening spending still needs a rules ruling; unsupported claims fail without altering balances. CHOAM games remain disabled until the remaining powers and full interaction audit are complete.',
|
||||
'Inflation retains its two-turn lifecycle and original wealth threshold. Homeworld additions remain bank-funded regardless of the ordinary payer. Controls explain the provisional shortfall rule without exposing private wallets. CHOAM starts remain labelled previews; full faction, combination and official insolvency acceptance remain unfinished.',
|
||||
],
|
||||
related: [
|
||||
'charity',
|
||||
@@ -4098,6 +4098,60 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'choam-worthless',
|
||||
'bg-charity-karama',
|
||||
],
|
||||
checklist: [
|
||||
{
|
||||
"area": "Implementation",
|
||||
"status": "Partial",
|
||||
"detail": "Beneficiary-aware payer fixes CHOAM self-claims. A private quote caps another CHOAM wallet at the ordinary contribution and supplies the provisional bank residual, retaining full entitlement, BG response, once-turn claims, Inflation and Homeworld bank portions. No debt or new persisted field.",
|
||||
"evidence": [
|
||||
"game/charity.ts",
|
||||
"game/engine.ts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"area": "Player controls",
|
||||
"status": "Partial",
|
||||
"detail": "Actual persistent claim controls distinguish ordinary bank payment from the constant provisional CHOAM fallback. Busy, pending income, completed/zero claims, Truthtrance and Nexus Traitor interruption remain blocked; no rival balance is read.",
|
||||
"evidence": [
|
||||
"components/charity-claim.tsx",
|
||||
"components/game-table.tsx",
|
||||
"tests/charity-claim-controls.test.tsx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"area": "AI",
|
||||
"status": "Partial",
|
||||
"detail": "Eight existing-profile Basic/Advanced paths accept16 legal Charity proposals for both low factions through the same settlement. Strategy, full AI and difficulty calibration remain deferred.",
|
||||
"evidence": [
|
||||
"game/bots.ts",
|
||||
"docs/CHOAM_CHARITY_FUNDING.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"area": "Documentation",
|
||||
"status": "Partial",
|
||||
"detail": "Separates the sourced self-bank composition from the unconfirmed bank-residual remedy, including the cost-shifting limitation and unfinished full-rule gates.",
|
||||
"evidence": [
|
||||
"docs/CHOAM_CHARITY_FUNDING.md",
|
||||
"docs/RULE_DECISIONS.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"area": "Verification",
|
||||
"status": "Partial",
|
||||
"detail": "Nine files88/88, types/lint/build, eight actual control/gameplay/GameTable SSR journeys and eight minimal-policy continuations pass. Genuine24/27-action public setup/income/bribe sources preserve original claims, physical pieces/cards and deferred receipts through Bidding. Browser/deployed, complete modes, broader combinations and comprehensive assurance remain open.",
|
||||
"evidence": [
|
||||
"tests/fixture-choam-charity-funding.ts",
|
||||
"tests/choam-charity-funding-engine.test.ts",
|
||||
"tests/charity-funding-quote.test.ts",
|
||||
"tests/charity-claim-controls.test.tsx",
|
||||
"tests/choam-charity.test.ts",
|
||||
"tests/homeworld-charity-engine.test.ts",
|
||||
"tests/choam-inflation.test.ts",
|
||||
"tests/tech-tokens.test.ts"
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'bg-charity-karama',
|
||||
|
||||
Reference in new issue
Block a user