A solitaire card slides neatly into its new column. Then the player presses Undo, and the card returns, but the card underneath stays exposed. The animation looks finished. The game is wrong. This is the kind of bug that makes a familiar card game an unexpectedly useful programming project: one small action can change several pieces of information at once.
Open PlaySolitaire and use a classic Klondike game as a concrete reference for what a player sees: the columns, the stock, the foundations and controls such as Undo. The design below is a proposal for building your own game, not a description of that website’s internal code. Its central question is simple: what must the program remember so that every move, including its reversal, remains consistent?
Define the rules before the interface
Start with one version of Klondike. In the tableau, the seven main columns, exposed cards build downward in alternating colors. The four foundations build upward by suit from Ace to King. A King, or a valid sequence starting with one, can occupy an empty tableau column. Those rules give a move validator something precise to check.
Write down the variant choices as well. Drawing one card from the stock and drawing three create different access to the waste pile. A limit on trips through the stock changes play again. Decide whether foundation cards can return to the tableau and what counts as a scored action. Ambiguity here becomes inconsistent behavior later, especially when hints, automatic moves and manual input all reach the same board.
Keep the cards separate from their pictures
Represent a card with a stable identity, a suit and a rank. Store its position in an ordered pile, together with the information needed to decide whether it is exposed. The board model should describe all seven tableau columns, the stock, the waste and the four foundations without depending on where a card happens to be drawn on the screen.
That separation lets you answer useful questions without opening a browser window. Where is the Queen of Hearts? How many cards remain in the stock? Does every card appear exactly once? A visual component can render the answers, but it should not become the only place they exist. Otherwise, changing the layout for a phone can accidentally change the rules that determine whether a move is legal.
Give every move one decision point
Imagine moving a red seven and the valid sequence below it onto a black eight. The request should identify the source pile, the starting card and the destination. A single rules function can check that the selected sequence is exposed, correctly ordered and compatible with the destination before changing anything. An invalid request should leave the board untouched.
For a valid request, update the source and destination together. If removing the sequence exposes a hidden card that must turn over, include that change in the same operation. Treat the whole action as one decision, not several unrelated screen updates. The result is easier to reason about: either the complete legal move happened, or none of it did. Intermediate arrangements should never become another input’s starting point.
Decide what Undo promises
Return to the Undo control in the browser example and think about the promise its label makes. For your own implementation, decide which parts of the previous position will return: pile order, exposed cards, stock pass count and any move counter or score affected by the action. A timer may deliberately keep running, but that should be a defined rule rather than an accidental omission.
A straightforward starting design saves an independent snapshot before each accepted move. Restoring that snapshot can recover related changes together. Do not mistake a shallow copy of nested arrays for an independent history entry: later edits can still alter shared objects. For a model containing supported plain data, JavaScript’s structuredClone can produce a deep copy. Keep functions and screen elements outside the saved model, and handle unsupported values explicitly.
Handle the stock as a real sequence
The stock is an easy place to create a bug that only appears several minutes into a game. With a draw of three, specify the order in which cards enter the waste and which exposed card can be played. Test what happens when only one or two cards remain. Recycling the waste must also preserve the ordering required by your chosen rules.
Do not hide these details inside an animation callback. A draw is a game action with a before state and an after state, just like a tableau move. Undoing it should restore the exact earlier piles and any pass counter. A test that checks only the number of remaining cards will miss an ordering error, even though that error can change the player’s available moves.
Make different controls request the same move
Dragging is not the rule system. A player who clicks a card and then a destination should reach the same validator as someone who drags it. The same principle applies if you add keyboard controls. Different input methods can gather the source and destination differently, but they should not each maintain their own interpretation of which placements are allowed.
A click or tap alternative also makes a precise dragging movement unnecessary. W3C accessibility guidance specifically discusses alternatives to dragging, including selecting an item and then choosing its destination. For a card game, make the selected card visible and let the player cancel a selection. Test longer columns as well as the opening layout, because overlapping cards can make an otherwise clear control difficult to target.
Let animation follow an accepted change
Once the model accepts a move, the interface can animate the transition between positions. Decide what happens if the player clicks again while cards are moving. Ignoring input until that transition finishes is one possible policy. Queuing it is another, but a queued request must be validated against the current board when it is actually processed.
Either approach is clearer than allowing animation timing to decide which card belongs to which pile. Test rapid repeated clicks and Undo during a transition. A slower device or a busy browser tab can expose assumptions that never appear during careful testing on a fast laptop. The important outcome is not that every animation completes at the same speed, but that the final visible board agrees with the model.
Treat a saved game as untrusted input
If you add resume support, save enough information to reconstruct the position and its rule settings, not just the visible cards. Include a format version so that a later release can distinguish an old save from the structure it now expects. A draw setting or stock pass counter can matter just as much as a card’s current column.
On loading, check that the data has the expected shape and contains a valid set of cards without duplicates or omissions. Stored data may be old, incomplete or manually altered. If it cannot be restored safely, explain that and offer a new game rather than producing a broken board. Also define whether a resumed game retains Undo history; silently changing that promise can confuse the player.
Test positions, not just finished games
You do not need to win a full game to test an empty column. Construct a small controlled position and verify that it accepts a King but rejects another rank. Create another position for a foundation move with the wrong suit, and another where moving a sequence should expose a hidden card. Each setup isolates a rule you can explain.
Then check broader properties after every accepted action: the game still contains all 52 unique cards, the relevant pile order is correct, and no card appears in two places. For reversible moves, compare the saved state with the result of making the move and undoing it. Keep the agreed timer and scoring policy in that comparison. These checks turn a vague complaint about a strange game into a reproducible failure.
Build the dependable version first
A useful first milestone is not an elaborate card animation or a new framework. It is a board that accepts legal moves, refuses illegal ones and returns to the intended position when asked. Add one rule variation at a time, keeping the same tests for the behavior that should not change.
Solitaire makes this discipline visible. A player can see the red seven, the black eight and the card underneath, so a partial update is not an abstract technical mistake. It breaks a move they understand. Building around an explicit state model gives you a way to preserve that understanding while the interface, controls and visual style evolve.