Support Truthtrance in Richese Once Around bidding
This commit is contained in:
1 parent
65a1936b94
commit
090d764f6f
18 files changed
+1003
-7
No files matched your search
@@ -1,4 +1,5 @@
|
||||
'use client';
|
||||
import { RicheseBidPromisePanel } from './richese-bid-promise-panel';
|
||||
import { TravelPromises } from './travel-promises';
|
||||
import { TurnActionPromises } from './turn-action-promises';
|
||||
import { MixedTraitorPromises } from './mixed-traitor-promises';
|
||||
@@ -4332,12 +4333,15 @@ export function GameTable({
|
||||
)}
|
||||
{g.phase === 2 && <CharityClaim game={g} act={act} busy={busy} />}
|
||||
{g.phase === 3 && g.richeseAuction && (
|
||||
<>
|
||||
<RicheseBidPromisePanel promises={g.richeseBidPromises} guide={g.richeseBidPromiseGuide} game={g} act={act} busy={busy} />
|
||||
<RicheseAuctionLot
|
||||
key={`${g.richeseAuction.event}-${g.me}`}
|
||||
game={g}
|
||||
act={act}
|
||||
busy={busy}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{g.phase === 3 && !g.richeseAuction && g.auction && (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
'use client';
|
||||
|
||||
import { Input } from './ui/input';
|
||||
|
||||
/** Minimum is public, positive and precisely representable in the action. */
|
||||
export function richeseBidMinimumError(value: unknown): string | null {
|
||||
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0
|
||||
? null : 'Choose a positive whole bid minimum.';
|
||||
}
|
||||
|
||||
export function RicheseBidPromiseFields(props: {
|
||||
minimum: number;
|
||||
onMinimum: (value: number) => void;
|
||||
disabled: boolean;
|
||||
}) {
|
||||
const error = richeseBidMinimumError(props.minimum);
|
||||
return <fieldset className="grid gap-2" disabled={props.disabled} aria-label="Richese Once Around bid condition">
|
||||
<legend>Bid in this Once Around auction</legend>
|
||||
<label htmlFor="truth-richese-bid-minimum">Minimum bid in spice</label>
|
||||
<Input id="truth-richese-bid-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-richese-bid-help"
|
||||
onChange={event => {
|
||||
const value = event.currentTarget.valueAsNumber;
|
||||
if (!props.disabled && !richeseBidMinimumError(value)) props.onMinimum(value);
|
||||
}} />
|
||||
{error && <p role="alert">{error}</p>}
|
||||
<p className="fine" id="truth-richese-bid-help">
|
||||
This asks about the bidder’s remaining opportunity in this Once Around lot.
|
||||
Earlier declarations do not count; a qualifying bid does not promise a win
|
||||
or change the winning payment.
|
||||
</p>
|
||||
</fieldset>;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
'use client';
|
||||
|
||||
import type { Action } from '@/game/engine';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
export type RicheseBidPromiseReceipt = {
|
||||
id: string;
|
||||
turn: number;
|
||||
event: string;
|
||||
minimum: number;
|
||||
answer: boolean;
|
||||
fulfilled?: boolean;
|
||||
released?: boolean;
|
||||
};
|
||||
export type RicheseBidPromisePanelProps = {
|
||||
promises: RicheseBidPromiseReceipt[];
|
||||
guide: { action: Action } | null;
|
||||
game: { turn: number; richeseAuction: { event: string } | null };
|
||||
act: (action: Action) => void;
|
||||
busy: boolean;
|
||||
};
|
||||
|
||||
/** The auction owns the physical card, actor, funding and final legality. */
|
||||
export function RicheseBidPromiseGuide(props: RicheseBidPromisePanelProps) {
|
||||
const { game, guide } = props;
|
||||
if (!guide) return null;
|
||||
const auction = game.richeseAuction, turn = game.turn, action = guide.action;
|
||||
const auctionEvent = action.event, id = action.richeseBidPromiseId;
|
||||
const signature = JSON.stringify(action);
|
||||
const isCurrent = () => props.game === game && props.guide === guide &&
|
||||
props.guide.action === action && JSON.stringify(action) === signature &&
|
||||
game.turn === turn && !!auction && game.richeseAuction === auction &&
|
||||
typeof auctionEvent === 'string' && auction.event === auctionEvent &&
|
||||
typeof id === 'string' && action.type === 'richeseBid' &&
|
||||
props.promises.some(promise => promise.id === id && promise.turn === turn &&
|
||||
promise.event === auctionEvent && !promise.fulfilled && !promise.released);
|
||||
const canAct = () => !props.busy && isCurrent();
|
||||
if (!isCurrent()) return null;
|
||||
return <Button className="game-action min-h-11 whitespace-normal" data-testid="richese-bid-promise-guide"
|
||||
disabled={!canAct()} onClick={() => { if (canAct()) props.act(action); }}>
|
||||
{typeof action.amount === 'number' && action.amount > 0
|
||||
? `Bid ${action.amount} spice while honoring your answer`
|
||||
: 'Pass this lot while honoring your answer'}
|
||||
</Button>;
|
||||
}
|
||||
|
||||
/** The server supplies only this player's own receipts and a legal guide. */
|
||||
export function RicheseBidPromisePanel(props: RicheseBidPromisePanelProps) {
|
||||
if (!props.promises.length) return null;
|
||||
const waiting = props.promises.some(promise => promise.turn === props.game.turn &&
|
||||
promise.event === props.game.richeseAuction?.event && !promise.fulfilled && !promise.released);
|
||||
return <section className="notice space-y-3" aria-label="Your Richese Once Around bid promises">
|
||||
<h3>Your Once Around bid promises</h3>
|
||||
<ul>{props.promises.map(promise => <li key={promise.id}>
|
||||
Turn {promise.turn}: You answered {promise.answer ? 'Yes' : 'No'} to bidding at least {promise.minimum} spice in this Once Around lot.
|
||||
{promise.released ? ' Released: this commitment is no longer possible.'
|
||||
: promise.fulfilled ? ' Fulfilled: this commitment is complete.' : ' This answer is binding for your remaining opportunity.'}
|
||||
</li>)}</ul>
|
||||
<p>Yes requires a qualifying bid while possible. No forbids a qualifying bid.
|
||||
The original auction order and payment rules still apply.</p>
|
||||
{waiting && <RicheseBidPromiseGuide {...props} />}
|
||||
</section>;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
'use client';
|
||||
import { RicheseBidPromiseFields, richeseBidMinimumError } from './richese-bid-promise-fields';
|
||||
|
||||
import { TravelClaimFields, travelClaimInputError } from './travel-claim-fields';
|
||||
import { TravelAnswers } from './travel-promises';
|
||||
@@ -97,7 +98,7 @@ export function Truthtrance({
|
||||
g.players.find((p) => p.id !== me.id)!.id,
|
||||
);
|
||||
const [kind, setKind] = useState<
|
||||
'fact' | 'freeform' | 'battlePlan' | 'shipment' | 'mixedShipment' | 'movement' | 'traitorCall' | 'bid' | 'phaseBid' | 'revival' | 'bribe' | 'alliance' | 'leaderRevival' | 'revivalExpression' | 'bribeExpression' | 'movementExpression' | 'mixedMovement' | 'mixedBribe' | 'mixedRevival' | 'mixedBid' | 'allianceExpression' | 'mixedAlliance' | 'mixedBattlePlan' | 'mixedTraitorCall' | 'travel' | 'turnActions'
|
||||
'fact' | 'freeform' | 'battlePlan' | 'shipment' | 'mixedShipment' | 'movement' | 'traitorCall' | 'bid' | 'phaseBid' | 'richeseBid' | 'revival' | 'bribe' | '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 ?? '');
|
||||
@@ -242,6 +243,7 @@ export function Truthtrance({
|
||||
: kind === 'mixedBid' ? { kind, target, turn: bidClaim.turn, lot: bidClaim.lot, mixed: mixedBid }
|
||||
: kind === 'bid' ? { kind, target, ...bidClaim }
|
||||
: kind === 'phaseBid' ? { kind, target, ...phaseBidClaim }
|
||||
: kind === 'richeseBid' ? { kind, target, turn:g.turn, event:g.richeseAuction?.event ?? '', minimum:bidMinimum }
|
||||
: kind === 'shipment'
|
||||
? {
|
||||
kind,
|
||||
@@ -426,6 +428,13 @@ export function Truthtrance({
|
||||
{button(`Answer ${answer === 'yes' ? 'Yes' : answer === 'no' ? 'No' : 'I don’t know'}`, { type: 'truthAnswer', answer })}
|
||||
</div>)}
|
||||
</>
|
||||
) : w.question!.kind === 'richeseBid' ? (
|
||||
<>
|
||||
<p className="notice">Yes requires a qualifying bid in your one remaining Once Around opportunity while possible. No forbids a qualifying bid. Winning still requires payment.</p>
|
||||
{(g.truthRicheseBidAnswers ?? []).map(answer => <div key={answer}>
|
||||
{button(`Answer ${answer === 'yes' ? 'Yes' : answer === 'no' ? 'No' : 'I don’t know'}`, { type: 'truthAnswer', answer })}
|
||||
</div>)}
|
||||
</>
|
||||
) : w.question!.kind === 'phaseBid' ? (
|
||||
<>
|
||||
<p className="notice">Yes requires one qualifying ordinary bid before this turn’s Bidding closes while possible. No forbids all qualifying later bids. A bid does not promise a win; winning still requires payment.</p>
|
||||
@@ -589,6 +598,7 @@ export function Truthtrance({
|
||||
<option value="revival">Bind ordinary force revivals this turn</option>
|
||||
<option value="bid">Bind a bid in this ordinary lot</option>
|
||||
<option value="phaseBid">Bind a bid this Bidding phase</option>
|
||||
<option value="richeseBid">Bind a bid in this Once Around lot</option>
|
||||
<option value="mixedBid">Combine current facts and ordinary bids</option>
|
||||
<option value="traitorCall">Bind this battle’s Traitor declaration</option>
|
||||
<option value="mixedTraitorCall">Combine current facts and this Traitor declaration</option>
|
||||
@@ -732,6 +742,12 @@ export function Truthtrance({
|
||||
{!g.truthRevivalTargets.includes(target) && <p className="notice">Choose a respondent whose native Revival opportunity remains this turn and finish pending decisions. Registered native Basic and Advanced tables without optional modules are supported first.</p>}
|
||||
<p className="notice" aria-label="Public revival question preview">{truthQuestionText(question, leaderName, locationName, name)}</p>
|
||||
</>
|
||||
) : kind === 'richeseBid' ? (
|
||||
<>
|
||||
<RicheseBidPromiseFields minimum={bidMinimum} onMinimum={setBidMinimum} disabled={busy || !g.richeseAuction} />
|
||||
{!g.truthRicheseBidTargets.includes(target) && <p className="notice">Choose a bidder with a remaining Once Around opportunity.</p>}
|
||||
<p className="notice" aria-label="Public Once Around bid question preview">{truthQuestionText(question, leaderName, locationName, name)}</p>
|
||||
</>
|
||||
) : kind === 'phaseBid' ? (
|
||||
<>
|
||||
<PhaseBidClaimFields value={phaseBidClaim} disabled={busy}
|
||||
@@ -877,6 +893,7 @@ export function Truthtrance({
|
||||
(kind === 'revival' && (!!revivalClaimInputError(revivalClaim) || !g.truthRevivalTargets.includes(target))) ||
|
||||
(kind === 'mixedBid' && (!!mixedBidError || !g.truthMixedBidTargets.includes(target))) ||
|
||||
(kind === 'bid' && (!!bidClaimInputError(bidClaim) || !g.truthBidTargets.includes(target))) ||
|
||||
(kind === 'richeseBid' && (!!richeseBidMinimumError(bidMinimum) || !g.richeseAuction || !g.truthRicheseBidTargets.includes(target))) ||
|
||||
(kind === 'phaseBid' && (!!phaseBidClaimInputError(phaseBidClaim) || !g.truthPhaseBidTargets.includes(target))) ||
|
||||
(kind === 'turnActions' && (!!turnActionsError || !g.truthTurnActionTargets.includes(target))) ||
|
||||
(kind === 'travel' && (!!travelError || !g.truthTravelTargets.includes(target))) ||
|
||||
|
||||
Reference in new issue
Block a user