'use client' import { useEffect, useRef, useState, type KeyboardEvent, type PointerEvent } from 'react' import { cn } from '../../lib/cn' import { usePrefersReducedMotion } from '../../lib/motion' import { Button } from '../Button' import { SegmentedControl } from '../SegmentedControl' export type SolitaireSuit = 'hearts' | 'diamonds' | 'clubs' | 'spades' export type SolitaireDraw = 1 | 3 export interface SolitaireResult { moves: number score: number seconds: number } export interface SolitaireProps { /** Cards turned from the stock at a time, for the first game. The picker changes it after that. */ defaultDraw?: SolitaireDraw /** Seed the shuffle to replay a deal. Leave it out for a random one each game. */ seed?: number /** Called once when all four foundations are complete. */ onWin?: (result: SolitaireResult) => void /** Merged last, so it wins. */ className?: string } interface Card { id: string suit: SolitaireSuit rank: number up: boolean } interface Table { stock: Card[] waste: Card[] foundations: Card[][] tableau: Card[][] score: number moves: number } /** Where cards live: `stock`, `waste`, `f0`–`f3`, `t0`–`t6`. */ type Pile = string const SUITS: SolitaireSuit[] = ['hearts', 'diamonds', 'clubs', 'spades'] const SYMBOL: Record = { hearts: '♥', diamonds: '♦', clubs: '♣', spades: '♠' } const RANKS = ['', 'A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'] const RANK_NAMES = ['', 'ace', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'jack', 'queen', 'king'] const red = (card: Card) => card.suit === 'hearts' || card.suit === 'diamonds' const cardName = (card: Card) => (card.up ? `${RANK_NAMES[card.rank]} of ${card.suit}, face up` : 'face-down card') const shortName = (card: Card) => `${RANK_NAMES[card.rank]} of ${card.suit}` const TOP_ROW: Pile[] = ['stock', 'waste', 'f0', 'f1', 'f2', 'f3'] const BOTTOM_ROW: Pile[] = ['t0', 't1', 't2', 't3', 't4', 't5', 't6'] const clock = (seconds: number) => `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}` /** mulberry32: small, fast, and good enough to shuffle cards reproducibly. */ function random(seed: number) { let state = seed >>> 0 return () => { state = (state + 0x6d2b79f5) >>> 0 let t = state t = Math.imul(t ^ (t >>> 15), t | 1) t ^= t + Math.imul(t ^ (t >>> 7), t | 61) return ((t ^ (t >>> 14)) >>> 0) / 4294967296 } } function deal(seed: number): Table { const next = random(seed) const deck: Card[] = SUITS.flatMap((suit) => Array.from({ length: 13 }, (_, index) => ({ id: `${suit}-${index + 1}`, suit, rank: index + 1, up: false }))) for (let index = deck.length - 1; index > 0; index -= 1) { const swap = Math.floor(next() * (index + 1)) ;[deck[index], deck[swap]] = [deck[swap], deck[index]] } const tableau = Array.from({ length: 7 }, (_, pile) => deck.splice(0, pile + 1).map((card, index) => ({ ...card, up: index === pile }))) return { stock: deck, waste: [], foundations: [[], [], [], []], tableau, score: 0, moves: 0 } } const clone = (table: Table): Table => ({ ...table, stock: table.stock.slice(), waste: table.waste.slice(), foundations: table.foundations.map((pile) => pile.slice()), tableau: table.tableau.map((pile) => pile.slice()), }) function cardsOf(table: Table, pile: Pile): Card[] { if (pile === 'stock') return table.stock if (pile === 'waste') return table.waste if (pile[0] === 'f') return table.foundations[Number(pile[1])] return table.tableau[Number(pile[1])] } const top = (list: T[]) => list[list.length - 1] /** A run can be lifted if it is face up and alternates colour going down by one. */ function isRun(cards: Card[]) { return cards.every((card, index) => card.up && (index === 0 || (red(card) !== red(cards[index - 1]) && card.rank === cards[index - 1].rank - 1))) } function canDrop(table: Table, cards: Card[], to: Pile) { const target = cardsOf(table, to) const lead = cards[0] if (!lead) return false if (to[0] === 'f') { if (cards.length !== 1) return false const head = top(target) return head ? head.suit === lead.suit && lead.rank === head.rank + 1 : lead.rank === 1 } if (to[0] === 't') { const head = top(target) return head ? head.up && red(head) !== red(lead) && lead.rank === head.rank - 1 : lead.rank === 13 } return false } /** Standard scoring, from the version that shipped with Windows. */ function points(from: Pile, to: Pile) { if (to[0] === 'f') return from[0] === 'f' ? 0 : 10 if (from === 'waste' && to[0] === 't') return 5 if (from[0] === 'f' && to[0] === 't') return -15 return 0 } /** Move `count` cards from the end of one pile to another, turning up what is uncovered. */ function moveCards(table: Table, from: Pile, count: number, to: Pile): Table { const next = clone(table) const source = cardsOf(next, from) const moving = source.splice(source.length - count, count) cardsOf(next, to).push(...moving) next.score = Math.max(0, next.score + points(from, to)) const exposed = top(source) if (from[0] === 't' && exposed && !exposed.up) { source[source.length - 1] = { ...exposed, up: true } next.score += 5 } next.moves += 1 return next } function foundationFor(table: Table, card: Card): Pile | null { for (let index = 0; index < 4; index += 1) if (canDrop(table, [card], `f${index}`)) return `f${index}` return null } const complete = (table: Table) => table.foundations.every((pile) => pile.length === 13) /** With the stock and waste gone and every card face up, the rest always plays out. */ const solvable = (table: Table) => !table.stock.length && !table.waste.length && table.tableau.every((pile) => pile.every((card) => card.up)) function describePile(table: Table, pile: Pile) { const cards = cardsOf(table, pile) if (pile === 'stock') return `Stock, ${cards.length} ${cards.length === 1 ? 'card' : 'cards'}${cards.length ? '' : ', turn the waste back over'}` if (pile === 'waste') return `Waste${cards.length ? `, top card ${cardName(top(cards))}` : ', empty'}` if (pile[0] === 'f') return `Foundation ${Number(pile[1]) + 1}${cards.length ? `, ${cardName(top(cards))}` : ', empty'}` const down = cards.filter((card) => !card.up).length const up = cards.filter((card) => card.up) return `Tableau ${Number(pile[1]) + 1}${cards.length ? `: ${down ? `${down} face-down ${down === 1 ? 'card' : 'cards'}; ` : ''}${up.map(cardName).join('; ')}` : ', empty'}` } function CardFace({ card, className }: { card: Card; className?: string }) { if (!card.up) { return (