'use client'; import { useId, useState } from 'react'; import type { Action, GameView } from '@/game/engine'; import type { RicheseAuctionMethod } from '@/game/richese-auction'; import { Button } from './ui/button'; import { Input } from './ui/input'; import { CardInspector, CardRules } from './card-inspector'; type Props = { game: GameView; act: (action: Action) => void; busy: boolean }; const buttonClass = 'min-h-11 whitespace-normal motion-reduce:transition-none'; const methodName = { normal: 'Normal auction', onceAround: 'Once Around', silent: 'Silent auction', }; /** Only supplied owner-projected hand/cache identities are selectable. */ export function RicheseAuctionDecision({ game, act, busy }: Props) { const id = useId(); const [cardId, setCardId] = useState(''); const [method, setMethod] = useState('onceAround'); const [direction, setDirection] = useState<'clockwise' | 'counterclockwise'>( 'clockwise', ); const [claim, setClaim] = useState(''); const bidding = game.richeseBidding; const decision = game.decision; const me = game.players.find((player) => player.id === game.me); if ( !bidding || !me || decision?.player !== game.me || bidding.owner !== game.me || ![ 'richeseBlackMarket', 'richeseDeclaration', 'richeseCache', 'richeseUnbid', ].includes(decision.kind) ) return null; const send = (fields: Omit) => { if (!busy) act({ type: 'decision', event: bidding.event, ...fields }); }; if (decision.kind === 'richeseDeclaration') return (

Choose whether your face-up cache card is auctioned before or after the ordinary cards. This declaration precedes Ixian auction inspection.

); if (decision.kind === 'richeseUnbid') { const room = (me.hand?.length ?? 0) < me.handLimit; return (

No player offered spice. Keep the cache card for free if your hand has room, or remove it from this game.

{game.richeseAuction?.card && ( <> )} {!room && (

Your hand is full; the card cannot be added.

)}
); } const blackMarket = decision.kind === 'richeseBlackMarket'; const cards = blackMarket ? (me.hand ?? []) : (bidding.cache ?? []); const selected = cards.find((card) => card.id === cardId) ?? cards[0]; const chosenMethod = !blackMarket && method === 'normal' ? 'onceAround' : method; return (

{blackMarket ? 'Offer a card from your hand without showing its face. Atreides may inspect it. An optional public claim may be truthful or a bluff. If no one pays spice, you keep the card.' : 'Choose one card from your separate cache. Its face is revealed to everyone when offered. Cache cards do not count toward your hand limit.'}

{bidding.offerBlocked && (

{bidding.offerBlocked}

)} {cards.length ? ( <> {selected && ( <> )} {chosenMethod === 'onceAround' && ( <> )} {blackMarket && ( <> setClaim(event.target.value)} placeholder="What you say you are selling" /> )}

Karama cannot acquire this auction’s card.{' '} {chosenMethod === 'silent' ? 'Every eligible player submits once; zero is allowed. Equal positive bids use storm order.' : chosenMethod === 'onceAround' ? 'Each eligible player has one bidding opportunity, normally ending with Richese.' : 'Bidding continues in order until the other bidders pass.'}

) : (

No card is available to offer.

)} {blackMarket && ( )}
); } /** Only projected public bids and this viewer's own sealed submission are rendered. */ export function RicheseAuctionLot({ game, act, busy }: Props) { const id = useId(); const auction = game.richeseAuction; const [draft, setDraft] = useState({ event: '', amount: '', ally: '0' }); if (!auction) return null; const me = game.me; const silent = auction.method === 'silent'; const minimum = silent ? 0 : auction.bid + 1; const amount = draft.event === auction.event && draft.amount !== '' ? Number(draft.amount) : minimum; const allyPayment = draft.event === auction.event ? Number(draft.ally) : 0; const update = (part: Partial) => setDraft({ event: auction.event, amount: String(amount), ally: String(allyPayment), ...part, }); const ownPayment = amount - allyPayment; const sellerBlocked = auction.source === 'blackMarket' && auction.owner === me; const allowedTurn = !auction.outcome && auction.eligible.includes(me) && (silent ? !auction.submitted.includes(me) : auction.active === me); const canBid = allowedTurn && Number.isSafeInteger(amount) && amount >= minimum && Number.isSafeInteger(allyPayment) && allyPayment >= 0 && allyPayment <= auction.allyAvailable && ownPayment >= 0 && ownPayment <= auction.ownAvailable && (!sellerBlocked || amount === 0); const name = (player: string | null) => game.players.find((p) => p.id === player)?.name ?? 'Player'; const send = (bid: number | null, ally: number) => { if (!busy) act({ type: 'richeseBid', event: auction.event, amount: bid, allyPayment: ally, }); }; return (

{auction.source === 'cache' ? 'Richese cache' : 'Black Market'} ·{' '} {methodName[auction.method]}

{auction.card ? ( <>

{auction.card.name}

{auction.source === 'blackMarket' && (

This face is private to you through ownership or authorized inspection.

)} ) : (

The Black Market card remains face down.

)}

{name(auction.owner)} is selling. Karama cannot acquire this card.

{auction.claim && (

Seller’s public claim: {auction.claim}

)} {silent ? ( <>

{auction.submitted.length} of {auction.eligible.length} bids submitted.

{auction.ownBid !== null && (

Your sealed bid: {auction.ownBid} spice, including{' '} {auction.ownAllyPayment} pledged by your ally.

)} {auction.revealedBids && (
    {Object.entries(auction.revealedBids).map(([player, bid]) => (
  • {name(player)}: {bid} spice
  • ))}
)} ) : (

Highest bid: {auction.bid} spice {auction.bidder ? ` from ${name(auction.bidder)}` : ''}.

)} {allowedTurn ? ( <>

Available funding: {auction.ownAvailable} own spice and{' '} {auction.allyAvailable} pledged spice.

{sellerBlocked && (

Positive Black Market bids by the seller are not supported.{' '} {silent ? 'Submit zero to continue.' : 'Pass to continue.'}

)} {!sellerBlocked && ( <> update({ amount: event.target.value })} /> {auction.allyAvailable > 0 && ( <> update({ ally: event.target.value })} /> )}

{canBid ? `Payment if you win: ${ownPayment} own spice + ${allyPayment} ally spice.` : 'Choose a valid bid and funding split within the available spice.'}{' '} {silent ? 'Your submitted amount stays private until all bids are in and cannot be changed.' : 'Raise the current bid or pass.'}

)} ) : (

{auction.outcome ? 'The auction result is being resolved.' : silent && auction.submitted.includes(me) ? 'Your bid is sealed. Waiting for the other bidders.' : !auction.eligible.includes(me) ? 'You are not eligible to bid on this card.' : `Waiting for ${name(auction.active)} to bid.`}

)}
); }