Skip to content
Go to file

Latest commit


Git stats


Failed to load latest commit information.
Latest commit message
Commit time


Solves games of Shenzhen Solitaire, a minigame available standalone or as part of Shenzhen I/O from Zachtronics. Here's an video intro to the gameplay, but if you know FreeCell it's basically that with three suits and "dragon" cards that block gameplay. And this glossary of Solitaire terms may be useful.

gameplay screenshot

Written for practice with Haskell and HaskellStack.


  • create Stack project
  • types for cards
  • create standard deck
  • Layout type
  • custom Show instances for Card, Layout
  • enter game Layout
  • shuffle deck to create Layout
  • Game type
  • Use standard terms
  • Move type for card moves, collecting dragons
  • move :: Game -> Move -> Game
    • move with MoveFromColumnToCell
    • move with MoveFromCellToColumn
    • move with BuildFromColumn
    • move with BuildFromCell
    • move with Pack
    • move with CollectDragons
  • replicate automatic build of released Cards
  • automatically build Flower
  • automatically build after player Move applied
  • automatically build at game start and between moves
  • Detect game win
  • Detect game loss
  • Generate list of possible Moves for a position
  • Filter possible Moves against Game history to avoid loops
  • Bug: solver loops infinitely at losing states
  • Take moves until game win/loss
  • QuickCheck that the number + distribution of cards in the game is constant
  • Bug: mayTakeTo takes to last instance of a Dragon, not first
  • Bug: Prelude.head: empty list
  • Bug: Exception: shouldn't be trying to build Flower
  • Bug: automatic building isn't always building the Flower
  • Move smart constructors should error on Flower
  • Bug: Exception: Non-Suited card on Foundation
  • Bug: Exception: element not in list, I warned you I was unsafe
  • showcols does not print empty space or __ for empty columns so columns shift left
  • [DragonCell] should be a type that exposes only one empty cell at a time, cut down on solution space obviated by canonicalize
  • Some kind of memoization to avoid solver re-attempting from known-losing positions
  • Run a hundred times, report statistics
  • Bug: trying to move Flower to cell, add tests

Cleanups and open questions:

  • Solver should filter to only pack to leftmost empty column
  • Never use head - maybe move to classy-prelude?
  • Organize code into modules, don't export constructors or the many unsafe utility functions for Moves
  • there must be a nicer way to express lastCardsOfRuns
  • Silence -Wincomplete-patterns and -Wincomplete-uni-patterns on util funcitons
  • Encode that Tableau has exactly one Foundation per Suit
  • Encode that Tableau has exactly one DragonCell per Suit
  • Encode that FlowerCell can only hold a Flower
  • Look at Bound or Enum for Rank
  • mayTakeTo and mkRunTo want some kind of help
  • The Move constructors must enforce validity to avoid passing around broken Move data, but then move has none. Does this make move clear or unsafe? What if this was more mature with Move in its own module not exporting the default constructor?
  • This ties into mayTakeTo and unsafeTakeTo. mkMove must use the former but move really wants unsafe to avoid unwrapping Maybe. I can't even see how to unwrap it, really.
  • And novelPossibleMoves, which is almost just move now (possibleMoves now) \\ previous game where now = current game
  • Require cells be used left-to-right to cut down state space of possible moves.
  • DragonCell could model explicitly that it's Card | CollectedDragons | Nothing
  • Is lost correct? It might be only correct in the context of a depth-first search, where a previous Tableau would've already been searched for a win.
  • Can I enforce that a Game only includes Move that apply to prior Tableau?
  • Can I ensure move is only given Moves generated from the Tableau they're being applied to?
  • nextRankForFoundation should call nextCardForFoundation
  • automaticBuild could be functor application
  • Would limiting to 100 moves cut off winning games? (Needs a second data structure; if I put timeouts in Losses it won't find shorter routes to Tableaus it happened to time out on.
  • Use some clever extension to derive Foundations's three stacks in data constructor from the three data constructors for Suit
  • On loss, try from Timeouts (carrying in existing Losses)
  • Record stats about the time it takes to solve games


Solver for Shenzen Solitaire




No releases published


No packages published
You can’t perform that action at this time.