import { Card } from "./Card"; import { COLOR_GLYPHS, COLOR_HEX, EXPEDITION_NAMES, cardRank, isHandshake, } from "../game/cards"; import { cardsOnBoard } from "../game/engine"; import { DISCARD, PLAY, type GameState, type PlaceType } from "../game/types"; interface BoardProps { state: GameState; selectedColor: number | null; selectedPlace: PlaceType | null; canPlay: boolean; canDraw: (source: number) => boolean; onChoosePlace: (place: PlaceType) => void; onCancelPlace: () => void; onDraw: (source: number) => void; } // Compact expedition cards are 124px tall with a default -94px overlap (30px // visible per card). A full 12-card expedition would need ~454px of stack // height, far more than the lane zone's available space, so long stacks // overlap more tightly to stay within the zone (and never cover the discard // button, score chip, or spill outside the board). const STACK_ZONE_BUDGET = 190; const COMPACT_CARD_HEIGHT = 124; function stackOverlapStyle(count: number): React.CSSProperties { if (count <= 1) return {}; const perCard = (STACK_ZONE_BUDGET - COMPACT_CARD_HEIGHT) / (count - 1); const overlap = Math.min(-94, Math.round(perCard - COMPACT_CARD_HEIGHT)); return { "--stack-overlap": `${overlap}px` } as React.CSSProperties; } function expeditionScore(cards: number[]): string | null { if (!cards.length) return null; const handshakes = cards.filter(isHandshake).length; const ranks = cards.reduce((sum, card) => sum + cardRank(card), 0); const score = (ranks - 20) * (1 + handshakes) + (cards.length >= 8 ? 20 : 0); return `${score >= 0 ? "+" : ""}${score}${handshakes ? ` ×${handshakes + 1}` : ""}`; } export function Board({ state, selectedColor, selectedPlace, canPlay, canDraw, onChoosePlace, onCancelPlace, onDraw, }: BoardProps) { return (
{EXPEDITION_NAMES.map((name, color) => { const opponent = cardsOnBoard(state, 1, color); const mine = cardsOnBoard(state, 0, color); const discard = state.piles[color]; const playTarget = selectedColor === color && selectedPlace === null && canPlay; const discardTarget = selectedColor === color && selectedPlace === null; const drawTarget = selectedPlace !== null && canDraw(color + 1); const discardChosen = selectedPlace === DISCARD && selectedColor === color; const playChosen = selectedPlace === PLAY && selectedColor === color; return (
{opponent.length ? (
{opponent.map((card) => )}
) : ( {COLOR_GLYPHS[color]} )} {expeditionScore(opponent) && {expeditionScore(opponent)}}
{name}
); })}
); }