The pack
solitaire
function solitaire(): Pack<{ solitaire: SolitaireApi }>
The solitaire pack: Klondike, FreeCell, Spider and any variant of your own, with a solver. Pure logic.
Helpers
createSolitaire
function createSolitaire(cfg: SolitaireConfig = {}): Solitaire
Creates a Solitaire table from a variant name (or a Variant) and a seed.
dealWinnable
function dealWinnable(variant: Variant, seed: number, opts: { maxNodes?: number; attempts?: number; strict?: boolean } = {}): Solitaire
Deal until the solver wins one inside its budget. Deterministic: seeds are
walked in a fixed order from seed, and game.seed reports the one used.
The result says what was proven: game.dealProof is 'proven' when an
attempt was won. When every attempt runs out of budget the last deal comes
back as 'unproven' (legal, perhaps winnable, but not known to be), or, with
strict: true, this throws instead. Raise maxNodes or attempts to make
proof more likely; the solver is a bounded search, so a hard variant (Spider
with several suits, say) can exhaust a small budget on most deals.
TRAP: 'proven' means "the solver found a win", not "every deal it
skipped was unwinnable" — a hard-but-winnable deal can be skipped.
DRAW
const DRAW: CardMove
Special move id meaning "click the stock".
freecell
function freecell(opts: { cells?: number } = {}): Variant
FreeCell — 8 columns, everything face-up, 4 free cells, no stock.
klondike
function klondike(opts: { drawCount?: 1 | 3; redeals?: number } = {}): Variant
Klondike — 7 columns dealt 1..7 (top face-up), 24-card stock, 4 foundations.
rateDeal
function rateDeal(game: Solitaire, opts: { maxNodes?: number } = {}): { minMoves: number; solved: boolean }
Difficulty of a deal: the solver's line length, or null if unsolved.
spider
function spider(opts: { suits?: 1 | 2 | 4 } = {}): Variant
Spider — 10 columns (54 face-down + the tops), a stock that deals one card to EVERY column, and 8 foundations filled by completing a K→A same-suit run.
spiderDeck
function spiderDeck(seed: number, suits: 1 | 2 | 4): Card[]
A Spider deck limited to suits (1, 2 or 4) distinct suits, still 8×13 cards.
Classes
Solitaire
class Solitaire
One solitaire table: piles, rules, undo history and solver. Create one with createSolitaire.
readonly variant: Variant— The rule table this game plays by.readonly seed: number— The seed that produced this deal.dealProof: DealProof— Whether the solver is known to win this deal AS DEALT (seeDealProof). Set bydealWinnable; it says nothing about the position after you have played, and aclone()starts'unchecked'.piles: Pile[]— Every pile on the table.moveCount: number— Moves made so far.constructor(variant: Variant, seed: number)pile(id: string): Pile | undefined— Returns the pile withid, or undefined.get tableau(): Pile[]— The tableau columns.get foundations(): Pile[]— The foundation piles.get cells(): Pile[]— The free cells. Empty for a variant without them.get stock(): Pile | undefined— The stock pile, or undefined for a variant with none.get waste(): Pile | undefined— The waste pile, or undefined for a variant with none.get redeals(): number— Waste-to-stock recycles left; -1 = unlimited.locate(uid: number): { pile: Pile; index: number } | null— Locate a card instance byuid→ its pile and index.get isWon(): boolean— Won when every card has reached a foundation.get isStuck(): boolean— No legal move AND no stock left to turn.maxMove(destIsEmptyColumn = false): number— Cards a FreeCell-style supermove may carry: (free cells + 1) × 2^(empty columns).isMovableRun(p: Pile, index: number): boolean— Is the run starting atindexliftable as one unit?canDrop(dest: Pile, card: Card, runLength = 1): boolean— Maycardland ondest(empty piles included)?move(from: string, index: number, to: string): MoveResult— Move the run atindexof pilefromonto pileto.draw(): MoveResult— Click the stock: draw to the waste, recycle it, or deal one to every column.autoMove(from: string, index: number): MoveResult— One-click smart move for the card/run at (from,index): a lone top card goes to a foundation if it can, otherwise the run takes the first tableau it fits, otherwise a free cell.collect(): MoveResult— Sweep every currently-playable card up to the foundations.canAutoComplete(): boolean— True once nothing is face-down: the rest is a formality, so offer auto-finish.undo(): boolean— Undo the last action (rolls the move counter back with it).redo(): boolean— Redo an undone action.findMoves(): CardMove[]— Every legal move, best-first.valuescores the classic priorities: uncover a face-down card > safe foundation > play from waste/cell > rearrange > risky foundation. ADRAW-shaped entry appears last when the stock can still be turned.canDraw(): boolean— Can the stock still be clicked?safeFoundationMove(): CardMove | null— A foundation move that can never be wrong: the card is an Ace/Two, or both opposite-colour foundations are already high enough that nothing below still needs it. Games use this for "auto-play the obvious"; the solver takes it WITHOUT branching, which is its single biggest pruner.hint(): CardMove | null— The single best move to suggest, or null when only a draw (or nothing) remains.autoStep(): boolean— Play one greedy move (or draw).false= won, stuck, or every remaining move would revisit a position it has already played. The anti-loop rule: a greedy player left to itself will shuffle one card between two columns forever, because both directions score the same. So auto-play remembers the positions it has been in and refuses to return to one; a rejected candidate is rolled back completely (no history entry, no move counted) and the next-best is tried instead.resetAutoplay(): void— Forget where auto-play has been (new deal, or the player took over).autoplay(opts: { maxMoves?: number } = {}): MoveResult[]— Greedy self-play to a conclusion — a win, a dead end, or a position with no un-played move left. Returns every result for headless assertions.key(): string— A canonical state key — tableau order doesn't matter, so columns sort.clone(): Solitaire— Deep clone (own history not copied) — the solver searches on a clone.heuristic(): number— How far from won this position looks — LOWER is better. Cards home count most; face-down cards and occupied cells are the classic obstructions. The solver's search order; also a usable "how am I doing" score.solve(opts: { maxNodes?: number; beam?: number } = {}): CardMove[] | null— Search for a winning line from the CURRENT position — greedy best-first with a bounded frontier (a beam), which suits solitaire far better than depth-first: the search never commits to one bad branch. Safe foundation moves are auto-played on every node, collapsing the trivial states. TRAP:nullmeans "not solved within the budget", NOT "unwinnable". RaisemaxNodes/beamfor a stricter (slower) answer.
Types
CardMove
interface CardMove
A legal move the solver/hint surface reports.
from: string— The id of the source pile.index: number— Index of the first moving card in the source pile.to: string— The id of the target pile.count: number— Cards that would travel.value: number— Heuristic value, higher = better (hints and the solver order by this).
CardStep
type CardStep = /** Cards left `from` (at `fromIndex`) and are now on top of `to`. */ | { type: 'move'; from: string; to: string; fromIndex: number; cards: Card[] } /** A newly exposed card turned face-up. */ | { type: 'flip'; pile: string; card: Card } /** Stock → waste (`cards` in the order they land). */ | { type: 'draw'; cards: Card[] } /** Waste → stock, face-down, for another pass. */ | { type: 'recycle'; count: number } /** Spider's stock: one card onto every column at once. */ | { type: 'dealAll'; deals: { pile: string; card: Card }[] } /** A complete K→A run left the tableau for a foundation. */ | { type: 'complete'; from: string; to: string; suit: Suit; cards: Card[] } | { type: 'win' } /** No legal move and no stock left. */ | { type: 'stuck' }
One visual beat of the journal. Replay each step at your own pace.
DealProof
type DealProof = 'unchecked' | 'proven' | 'unproven'
What is known about whether a deal can be won, as dealt:
'proven': the solver found a winning line from this deal within its budget.'unproven': the solver was asked and found none within its budget. The deal is legal and may well be winnable; it is simply not known to be. OnlydealWinnableandcreateSolitaire({ winnable: true })produce this.'unchecked': nobody asked the solver. A plainnew Solitaire(...)orcreateSolitaire(...).
MoveResult
interface MoveResult
The outcome of one action: the journal steps plus the end flags.
valid: boolean— True when the action was legal and applied.steps: CardStep[]— The journal steps, in play order.won: boolean— True when this action won the game.stuck: boolean— True when no legal move and no stock is left.
SolitaireApi
type SolitaireApi = typeof kit
game.solitaire — the variants, the engine, the solver and the deal checks, as one namespace.
SolitaireConfig
interface SolitaireConfig
Configures createSolitaire.
variant?: VariantName | Variant—'klondike','klondike-3','freecell','spider','spider-1','spider-2', or aVariantyou built yourself.seed?: number— The deal seed. Same seed, same deal. Default 1.winnable?: boolean— Look for a deal the solver has actually won (seedealWinnable). Check the game'sdealProof: when the attempts run out the last deal is returned as'unproven'unlessstrictis set.maxNodes?: number— Search budget per candidate whenwinnableis set.attempts?: number— Candidate seeds to try whenwinnableis set before giving up.strict?: boolean— Withwinnable: throw instead of returning an'unproven'deal when no attempt is won.
Variant
interface Variant
The rule table for one solitaire game. Pass one to createSolitaire.
name: string— The variant name, e.g. 'klondike'.tableau: number— Tableau columns.foundations: number— Foundation piles.cells: number— Free cells (FreeCell); 0 elsewhere.stock: 'draw' | 'dealAll' | 'none'— Does the table have a stock (and, for 'draw', a waste)?drawCount: number— Cards flipped per stock click (Klondike draw-1 / draw-3).redeals: number— Waste→stock recycles allowed; -1 = unlimited.acceptsEmpty(card: Card): boolean— May a card land on an EMPTY tableau column?canStack(moving: Card, onto: Card): boolean— Tableau build rule (what a card may land on).runRule(upper: Card, lower: Card): boolean— What makes a multi-card run liftable together.foundationRule: 'ace-up' | 'complete-run'— Foundations accept single cards (Klondike/FreeCell) or whole runs (Spider).deal(seed: number): Pile[]— Deal the opening table.
VariantName
type VariantName = keyof typeof VARIANTS | (string & {})
A built-in variant name, or any custom name.


