Prototype Truthtrance ally funding commitments
Verify and publish checkpoint container / container (push) Successful in 10m48s

This commit is contained in:
Quintine committed 2026-10-29 05:44:01 +11:00
1 parent 68643925e7
commit c414a03a19
14 files changed
+795 -4

No files matched your search

+10
View File
@@ -4280,6 +4280,16 @@ export function GameTable({
purpose={g.phase === 3 ? 'bid' : 'shipment'}
{...((shipmentAvailable || g.phase === 3 && g.auction?.active === me.id) ? {onContributionsChange:(shares:CoalitionFundingContribution[]) =>
setCoalitionShares({key:coalitionFundingKey, shares})} : {})} />}
{g.pledgePromises.some(promise => promise.turn === g.turn && promise.phase === g.phase &&
!promise.fulfilled && !promise.released) && <section className="notice" aria-label="Truthtrance ally-funding commitment">
<p>Your Truthtrance answer applies to future ally-funding updates this phase:</p>
<ul>{g.pledgePromises.filter(promise => promise.turn === g.turn && promise.phase === g.phase &&
!promise.fulfilled && !promise.released).map(promise => <li key={promise.event}>
{promise.answer ? 'Update at least ' : 'Do not update to at least '}{promise.minimum} spice for{' '}
{g.players.find(player => player.id === promise.ally)?.name ?? promise.ally}.
</li>)}</ul>
{g.pledgePromiseGuide && actionButton('Set the required ally funding', g.pledgePromiseGuide.action)}
</section>}
{me.ally && [3, 5].includes(g.phase) && (
<details className="alliance-funding">
<summary>
+37
View File
@@ -0,0 +1,37 @@
'use client';
import { Input } from './ui/input';
/** Validation is about the public question, never the respondent's private purse. */
export function pledgeClaimInputError(minimum: unknown): string | null {
return typeof minimum === 'number' && Number.isSafeInteger(minimum) && minimum > 0
? null : 'Choose a positive whole spice amount within the safe integer range.';
}
/** The recipient is the respondent's current public reciprocal ally. */
export function PledgeClaimFields(props: {
minimum: number;
allyName: string;
disabled: boolean;
onChange: (minimum: number) => void;
}) {
const error = pledgeClaimInputError(props.minimum);
return <fieldset className="grid gap-2" disabled={props.disabled} aria-label="Future ally funding condition">
<legend>Future ally funding update</legend>
<p>Current ally: {props.allyName}</p>
<label htmlFor="truth-pledge-minimum">Minimum future ally funding in spice</label>
<Input id="truth-pledge-minimum" type="number" min={1} max={Number.MAX_SAFE_INTEGER} step={1} required
disabled={props.disabled} value={Number.isFinite(props.minimum) ? props.minimum : ''}
aria-invalid={!!error} aria-describedby="truth-pledge-help"
onChange={event => {
const next = event.currentTarget.valueAsNumber;
if (!props.disabled && !pledgeClaimInputError(next)) props.onChange(next);
}} />
{error && <p role="alert">{error}</p>}
<p className="fine" id="truth-pledge-help">
Count an accepted funding update after the answer during this phase. Setting the same total again counts;
an earlier pledge alone does not. Funding sets spice aside for your ally and is not a payment or a promise
that your ally will spend it.
</p>
</fieldset>;
}
+21 -3
View File
@@ -42,6 +42,7 @@ import type { AllianceExpression } from '@/game/alliance-expression-promises';
import type { AllianceActionClaim } from '@/game/mixed-alliance-question';
import type { AllianceClaim } from '@/game/alliance-promises';
import { BribeClaimFields, bribeClaimInputError } from './bribe-claim-fields';
import { PledgeClaimFields, pledgeClaimInputError } from './pledge-claim-fields';
import type { BribeClaim } from '@/game/bribe-promises';
import { RevivalClaimFields, revivalClaimInputError } from './revival-claim-fields';
import type { RevivalClaim } from '@/game/revival-promises';
@@ -129,7 +130,7 @@ export function Truthtrance({
g.players.find((p) => p.id !== me.id)!.id,
);
const [kind, setKind] = useState<
'fact' | 'freeform' | 'charity' | 'battlePlan' | 'futureBattleLeader' | 'shipment' | 'mixedShipment' | 'movement' | 'traitorCall' | 'bid' | 'phaseBid' | 'richeseBid' | 'earlyRicheseBid' | 'revival' | 'bribe' | 'alliance' | 'leaderRevival' | 'revivalExpression' | 'bribeExpression' | 'movementExpression' | 'mixedMovement' | 'mixedBribe' | 'mixedRevival' | 'mixedBid' | 'allianceExpression' | 'mixedAlliance' | 'mixedBattlePlan' | 'mixedTraitorCall' | 'travel' | 'turnActions'
'fact' | 'freeform' | 'charity' | 'battlePlan' | 'futureBattleLeader' | 'shipment' | 'mixedShipment' | 'movement' | 'traitorCall' | 'bid' | 'phaseBid' | 'richeseBid' | 'earlyRicheseBid' | 'revival' | 'bribe' | 'pledgeAid' | 'alliance' | 'leaderRevival' | 'revivalExpression' | 'bribeExpression' | 'movementExpression' | 'mixedMovement' | 'mixedBribe' | 'mixedRevival' | 'mixedBid' | 'allianceExpression' | 'mixedAlliance' | 'mixedBattlePlan' | 'mixedTraitorCall' | 'travel' | 'turnActions'
>('fact');
const [revivalLeader, setRevivalLeader] = useState(g.allLeaders.find(leader =>
leader.faction === g.players.find(player => player.id === target)?.faction)?.id ?? '');
@@ -157,6 +158,8 @@ export function Truthtrance({
] });
const mixedAllianceError = mixedAllianceInputError(mixedAlliance, g, target);
const [bribeSelection, setBribeSelection] = useState<Pick<BribeClaim, 'recipient' | 'minimum'>>({ recipient: g.me, minimum: 3 });
const [pledgeMinimum, setPledgeMinimum] = useState(1);
const pledgeAlly = g.players.find(player => player.id === target)?.ally ?? '';
const bribeClaim: BribeClaim = { turn: g.turn, ...bribeSelection };
const bribeRespondent = g.players.find(player => player.id === target);
const bribeRecipients = g.players.filter(player => player.id !== target && player.id !== bribeRespondent?.ally &&
@@ -239,7 +242,7 @@ export function Truthtrance({
id === 'kwisatz' ? 'Kwisatz Haderach' : id === CHEAP_HERO_TRAITOR
? 'Cheap Hero / Heroine'
: (g.allLeaders.find((l) => l.id === id)?.name ?? id);
const name = (id: string) => g.players.find((p) => p.id === id)!.name;
const name = (id: string) => g.players.find((p) => p.id === id)?.name ?? id;
const locationName = (id: string) => g.combatLocations.find(place => place.id === id)!.name;
const question: TruthQuestion =
kind === 'fact'
@@ -273,6 +276,7 @@ export function Truthtrance({
: kind === 'mixedBribe' ? { kind, target, turn: g.turn, mixed: mixedBribe }
: kind === 'bribeExpression' ? { kind, target, turn: g.turn, claim: bribeExpression }
: kind === 'bribe' ? { kind, target, ...bribeClaim }
: kind === 'pledgeAid' ? { kind, target, turn: g.turn, phase: g.phase === 3 ? 3 : 5, ally: pledgeAlly, minimum: pledgeMinimum }
: kind === 'revival' ? { kind, target, ...revivalClaim }
: kind === 'mixedBid' ? { kind, target, turn: bidClaim.turn, lot: bidClaim.lot, mixed: mixedBid }
: kind === 'bid' ? { kind, target, ...bidClaim }
@@ -462,6 +466,13 @@ export function Truthtrance({
{button(`Answer ${answer === 'yes' ? 'Yes' : answer === 'no' ? 'No' : 'I don’t know'}`, { type: 'truthAnswer', answer })}
</div>)}
</>
) : w.question!.kind === 'pledgeAid' ? (
<>
<p className="notice">Yes requires a future funding update at or above the minimum while possible. No forbids such an update this phase. A previous pledge alone does not count, and the ally chooses whether to spend it.</p>
{(g.truthPledgeAnswers ?? []).map(answer => <div key={answer}>
{button(`Answer ${answer === 'yes' ? 'Yes' : 'No'}`, {type:'truthAnswer', answer})}
</div>)}
</>
) : w.question!.kind === 'revival' ? (
<>
<p className="notice">Yes requires the chosen total of future ordinary force returns while possible. No forbids reaching that total, including split requests. Only completed returns after this answer count. Keep the original revival funding and finish all remaining Yes answers before ending Revival.</p>
@@ -652,6 +663,7 @@ export function Truthtrance({
<option value="allianceExpression">Combine final alliance partners with AND/OR</option>
<option value="mixedAlliance">Combine current facts and final alliance partners</option>
<option value="bribe">Bind ordinary bribe payments this turn</option>
<option value="pledgeAid">Bind an ally-funding update this phase</option>
<option value="bribeExpression">Combine ordinary bribe payments with AND/OR</option>
<option value="mixedBribe">Combine current facts and ordinary payments</option>
<option value="mixedRevival">Combine current facts and ordinary Revival</option>
@@ -919,7 +931,7 @@ export function Truthtrance({
<TruthBattlePlanQuestion game={g} question={question} busy={busy}
act={act} onChange={setPlanClaim} />
)
) : kind === 'fact' ? (
) : kind === 'pledgeAid' ? null : kind === 'fact' ? (
question.kind === 'fact' && (
<TruthFactQuestion game={g} question={question} busy={busy}
act={act} onChange={setFact} />
@@ -961,6 +973,11 @@ export function Truthtrance({
)}
</>
)}
{kind === 'pledgeAid' && <>
<PledgeClaimFields minimum={pledgeMinimum} allyName={pledgeAlly ? name(pledgeAlly) : 'their current ally'}
disabled={busy || !g.truthPledgeTargets.includes(target)} onChange={setPledgeMinimum} />
{!g.truthPledgeTargets.includes(target) && <p className="notice">Choose a player with a mutual ally in a supported quiet Basic Bidding or Movement phase. Earlier definite future-action questions can make this question unavailable.</p>}
</>}
{!['fact', 'battlePlan', 'futureBattleLeader', 'shipment'].includes(kind) && button(
'Ask publicly',
{ type: 'truthAsk', question },
@@ -977,6 +994,7 @@ export function Truthtrance({
(kind === 'mixedBribe' && (!!mixedBribeError || !g.truthMixedBribeTargets.includes(target))) ||
(kind === 'bribeExpression' && (!!bribeExpressionError || !validBribeExpressionRecipients || !g.truthBribeExpressionTargets.includes(target))) ||
(kind === 'bribe' && (!!bribeClaimInputError(bribeClaim, bribeRecipients) || !g.truthBribeTargets.includes(target))) ||
(kind === 'pledgeAid' && (!!pledgeClaimInputError(pledgeMinimum) || !g.truthPledgeTargets.includes(target))) ||
(kind === 'revival' && (!!revivalClaimInputError(revivalClaim) || !g.truthRevivalTargets.includes(target))) ||
(kind === 'mixedBid' && (!!mixedBidError || !g.truthMixedBidTargets.includes(target))) ||
(kind === 'bid' && (!!bidClaimInputError(bidClaim) || !g.truthBidTargets.includes(target))) ||