Files
Dune/components/sapho-arrival-order.tsx
T
Quintine e457478f7d
Verify and publish checkpoint container / container (push) Canceled after 0s
Support Sapho ordering for shared arrivals
2026-10-29 05:29:51 +11:00

76 lines
3.5 KiB
TypeScript

'use client';
import type { Action } from '@/game/engine';
import { Button } from './ui/button';
/** Public order and responses; choices contain only this viewer's legal cards. */
export type SaphoArrivalOrderView = {
event: string;
revision: number;
order: string[];
passed: string[];
choices: { card: string; mode: 'first' | 'last' }[];
};
export type SaphoArrivalOrderProps = {
view: SaphoArrivalOrderView;
me: string;
busy: boolean;
name: (player: string) => string;
act: (action: Action) => void;
};
function current(props: SaphoArrivalOrderProps, view: SaphoArrivalOrderView,
event: string, revision: number): boolean {
return !props.busy && props.view === view && view.event === event &&
view.revision === revision && typeof event === 'string' && event.length > 0 &&
Number.isSafeInteger(revision) && revision >= 0 &&
view.order.includes(props.me) && !view.passed.includes(props.me);
}
/** Both original reaction owners can continue without revealing their hands. */
export function SaphoArrivalOrderPass(props: SaphoArrivalOrderProps) {
const { view } = props, event = view.event, revision = view.revision;
const canAct = () => current(props, view, event, revision);
return <Button className="game-action min-h-11 whitespace-normal" data-testid="sapho-arrival-order-pass"
disabled={!canAct()} onClick={() => { if (canAct()) props.act({ type: 'arrivalOrderPass', event, revision }); }}>
Continue without changing reaction order
</Button>;
}
export function SaphoArrivalOrderChoiceButton(props: SaphoArrivalOrderProps & {
choice: SaphoArrivalOrderView['choices'][number];
}) {
const { view, choice } = props, event = view.event, revision = view.revision;
const card = choice.card, mode = choice.mode;
const canAct = () => current(props, view, event, revision) &&
typeof card === 'string' && card.length > 0 && (mode === 'first' || mode === 'last') &&
choice.card === card && choice.mode === mode &&
view.choices.some(item => item.card === card && item.mode === mode);
return <Button className="game-action min-h-11 whitespace-normal" data-testid={`sapho-arrival-order-${mode}`}
disabled={!canAct()} onClick={() => {
if (canAct()) props.act({ type: 'saphoArrivalOrder', event, revision, card, mode });
}}>
Use Juice of Sapho to react {mode}
</Button>;
}
/** Do not infer eligibility from a hand: the server supplies the public window
* and only the current viewer's privately quoted card choices. */
export function SaphoArrivalOrder(props: SaphoArrivalOrderProps) {
const { view, me, name } = props;
const eligible = view.order.includes(me), passed = view.passed.includes(me);
return <section className="notice space-y-3" aria-label="Ambassador and Terror reaction order">
<h3>Ambassador and Terror reaction order</h3>
<p>Both reactions follow the same entry. Before either begins, their owners may continue or use an available order change.</p>
<p>Current order: {view.order.map((player, index) =>
<span key={player}>{index > 0 && ' → '}{name(player)}</span>)}.</p>
<p>{view.passed.length} of {view.order.length} players have finished this choice.</p>
<p className="fine">This ordering procedure is a provisional interpretation.</p>
{eligible && !passed ? <>
<SaphoArrivalOrderPass {...props} />
{view.choices.map(choice => <SaphoArrivalOrderChoiceButton {...props}
key={`${choice.card}:${choice.mode}`} choice={choice} />)}
</> : <p>{eligible ? 'You have finished this choice.' : 'Waiting for the reaction owners.'}</p>}
</section>;
}