Files
Dune/components/juice-of-sapho.tsx
T
Quintine cb691e1b14 Connect Sapho first and last battle-choice order
Juice of Sapho can change the remaining Battle-phase chooser order at clean boundaries. A chooser may occupy the physical defender slot and select the actual other combatant. Physical storm order, attackers, defenders and tie advantage remain intact. Full-roster phase priority survives aftermath, newly eligible Face Dancers and saved continuation. Controls and all four AI profiles consume the same projected choices.

Independent review verified private options, public geometry, exact physical disposal, rejected-action immutability, phase expiry and boundary-bound saved chooser integrity. Actual Basic/Advanced Face Dance regressions verify later replacement priority. Authenticated SQLite tests cover duplicate concurrent card requests, private restore and battle completion. Chooser/aggressor separation and phase duration remain explicit implementation interpretations; the separate aggressor mode and unresolved timing stay gated. Reinforcements remains disabled pending the documented reserve-cost question.

Types, lint, 4792 offline tests, build and 43 HTTP tests pass with unchanged source. Six fixed genuine Basic/Advanced faction samples finish in 3672 accepted actions and 97 periodic private JSON restores. They retain 82 rejected candidates at existing guarded arrival combinations; subsequent legal candidates finish. This is bounded playability evidence, not complete compliance or AI calibration.

The browser used Sapho first from a later physical circle, chose Emperor, sealed and resolved a battle, then refreshed into its next choice. Emperor retained the tie win. All 9 accepted actions replay exactly using the recorded random battle event; physical custody, all private JSON views and the original seat row pass. A private harness initially needed random-event replay binding and SQLite-row prototype normalization; no production fix or game reset was required.

After the reported outage, the server responded and the staged QA game restored exactly. All 970 opening games and their original seat/recovery records remain preserved (1004 after checks), including all 971 rooms present immediately before checks. SQLite integrity and foreign keys pass. No restart, database reset or recurring automation was needed.

Verified source tree: b699f80600f8e297595a8e756edf367a607ddd99707474c03231e6479bd2f389
2026-09-14 05:14:57 +10:00

100 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useId } from 'react';
import type { Action, GameView } from '@/game/engine';
import { richeseCardDefinition } from '@/game/richese-cards';
import { Button } from './ui/button';
import { CardInspector } from './card-inspector';
/** Reasons use public timing only; exact card reservation checks stay on the server. */
function unavailableReason(game: GameView) {
if (game.status !== 'playing')
return 'Order changes are available during play.';
if (game.truthtrance || game.response || game.decision || game.phaseOpening)
return 'Finish the current interaction before changing turn order.';
if (game.phase === 3) {
if (game.richeseAuction?.method === 'silent')
return 'Silent bids are simultaneous. These controls do not change their tie order.';
if (game.richeseAuction?.method !== 'onceAround')
return 'These bidding controls require a Once Around auction.';
return 'No order change is available for you in this lot. You must still be eligible and have not bid; first is available only before bidding begins. A card already committed elsewhere cannot be used.';
}
if (game.phase === 5)
return 'Movement order can change only before the current combined shipment and movement turn begins. First requires all remaining turns to be unstarted and the Advanced Guild to have finished or be absent. Completed turns stay completed. Already-held positions and committed cards are unavailable.';
if (game.phase === 6)
return game.battle
? 'Finish the current battle and its aftermath before changing the order of remaining battle choices. This does not change the current battle’s aggressor.'
: 'You need an unresolved battle and an available first or last position. Completed battles stay resolved, and a card committed elsewhere cannot be used.';
return 'These controls support Once Around bidding, movement order and remaining battle-choice order. Battle aggressor and other phase or auction modes remain unfinished.';
}
export function JuiceOfSapho({
game,
act,
busy,
}: {
game: GameView;
act: (action: Action) => void;
busy: boolean;
}) {
const id = useId();
const me = game.players.find((p) => p.id === game.me);
const card = me?.hand?.find(
(c) => richeseCardDefinition(c)?.card.effect === 'juiceOfSapho',
);
if (!card) return null;
const options = game.saphoOptions ?? [];
return (
<details className="my-4 min-w-0">
<summary className="min-h-11 cursor-pointer py-3">
Juice of Sapho: change turn order
</summary>
<div className="flex min-w-0 flex-col gap-4 py-3">
<p>
Choose one available order change, then discard Juice of Sapho. This
does not grant an extra bid, shipment, movement or battle.
</p>
<CardInspector card={card} />
{options.length ? (
options.map((option) => {
const once = option.scope === 'onceAround';
const battle = option.scope === 'battleOrder';
return (
<div
key={`${option.scope}-${option.event}-${option.mode}`}
className="flex min-w-0 flex-col gap-2"
>
<p>
{once
? `Bid ${option.mode} in the current Once Around lot. Each eligible player still has only one bidding opportunity.`
: battle
? `Choose your remaining battles ${option.mode}. Completed battles stay resolved. This changes who chooses next, not the aggressor or tie advantage of a battle. Other players may still choose battles against you before your turn.`
: `Take the ${option.mode} remaining combined shipment and movement turn. Completed turns stay completed${option.mode === 'last' ? ', and the Guild cannot move behind you' : ''}.`}
</p>
<Button
className="min-h-11 whitespace-normal motion-reduce:transition-none"
disabled={busy}
onClick={() => {
if (!busy) act({ type: 'card', card: card.id, ...option });
}}
>
{once ? 'Bid' : battle ? 'Choose battles' : 'Take your movement turn'} {option.mode} and
discard Juice of Sapho
</Button>
</div>
);
})
) : (
<p className="notice" id={`${id}-reason`}>
{unavailableReason(game)}
</p>
)}
<p className="text-sm">
Battle aggressor and other phase or auction modes are still
unfinished.
</p>
</div>
</details>
);
}