Phaser AE

Solitaire games

Pro packv3.0.0-02DGrid

The patience card games, Klondike, FreeCell and Spider, as three rule tables over one engine. Moves come back as a journal to animate, a hint is always a real move, and a solver deals games it has actually won.

This is a Pro pack. Upgrade to Phaser Pro to use it in your games.See Phaser Pro →

Released October 19, 2026

Solitaire games: a screenshot of its demo
A note about these graphics

Expansion packs don't come with graphics. This screenshot is one way of drawing what the pack does, made for its demo: none of the sprites, tiles or backgrounds in it are in the pack.

That's by design. A pack is a game's logic, mechanics and effects, and it works with whatever art you give it. Some packs draw effects, backdrops or 3D scenes of their own, but none carries a game's sprites.

So the look is yours. Make the art on phaser.io, bring your own, or let your agent draw it in code: the same pack can look like anything.

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 (see DealProof). Set by dealWinnable; it says nothing about the position after you have played, and a clone() 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 with id, 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 by uid → 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 at index liftable as one unit?
  • canDrop(dest: Pile, card: Card, runLength = 1): boolean — May card land on dest (empty piles included)?
  • move(from: string, index: number, to: string): MoveResult — Move the run at index of pile from onto pile to.
  • 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. value scores the classic priorities: uncover a face-down card > safe foundation > play from waste/cell > rearrange > risky foundation. A DRAW-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: null means "not solved within the budget", NOT "unwinnable". Raise maxNodes/beam for 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. Only dealWinnable and createSolitaire({ winnable: true }) produce this.
  • 'unchecked': nobody asked the solver. A plain new Solitaire(...) or createSolitaire(...).

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 a Variant you built yourself.
  • seed?: number — The deal seed. Same seed, same deal. Default 1.
  • winnable?: boolean — Look for a deal the solver has actually won (see dealWinnable). Check the game's dealProof: when the attempts run out the last deal is returned as 'unproven' unless strict is set.
  • maxNodes?: number — Search budget per candidate when winnable is set.
  • attempts?: number — Candidate seeds to try when winnable is set before giving up.
  • strict?: boolean — With winnable: 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.