DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

Building Klondike Solitaire from scratch: eleven arrays and one move() door

Klondike is the solitaire everyone has clicked through on an idle afternoon, and it looks like it would be miserable to build — 52 cards, drag-and-drop, and all those fiddly rules about what can sit on what. When I wrote it in vanilla JavaScript, it turned out to be far tidier than I feared. The entire game is a handful of arrays and a single function that every move passes through. Here is how it fits together.

A card is three facts

Every card in the deck carries only three pieces of information: its suit, its rank as a number from 1 (Ace) to 13 (King), and whether it is currently face-up.

const isRed = c => c.suit === "H" || c.suit === "D";   // colour is DERIVED
// a card: { suit:"H", rank:11, faceUp:false }  → J♥, face-down
Enter fullscreen mode Exit fullscreen mode

Notice colour is not stored. A card is red exactly when its suit is hearts or diamonds, so isRed computes it on demand. Deriving it removes a whole class of bug — there is no second field that could ever disagree with the suit.

Eleven piles, all just arrays

Klondike looks complicated, but it is only eleven stacks of cards: one stock, one waste, four foundations, and seven tableau columns. Every one of them is a plain array, and the top card is always the last element. A tiny lookup turns a pile id like "t3" or "f0" into the array it names:

function pile(id){
  if (id === "stock") return stock;
  if (id === "waste") return waste;
  if (id[0] === "f")  return foundations[+id.slice(1)];   // "f2" → foundations[2]
  if (id[0] === "t")  return tableau[+id.slice(1)];        // "t5" → tableau[5]
}
const top = arr => arr[arr.length - 1];
Enter fullscreen mode Exit fullscreen mode

The rest of the code never cares which kind of pile it is holding — it just asks pile(id) and works on an array.

Let the destination decide

The part people over-engineer is legality. I kept every rule with the destination, not the card being moved. A tableau builds down in alternating colour; a foundation builds up in a single suit. Two mirror-image functions, and each asks the top card one question:

function canDropTableau(moving, destId){
  const d = pile(destId);
  if (d.length === 0) return moving.rank === 13;            // empty → King only
  const t = top(d);
  return isRed(t) !== isRed(moving) && t.rank === moving.rank + 1;  // down + alt colour
}
function canDropFoundation(moving, destId){
  const d = pile(destId);
  if (d.length === 0) return moving.rank === 1;             // empty → Ace only
  const t = top(d);
  return t.suit === moving.suit && moving.rank === t.rank + 1;  // up + same suit
}
Enter fullscreen mode Exit fullscreen mode

One move() door

This is the piece I am happiest with. A move can arrive from a click, a drag, or a double-click, but all three funnel into one function. It refuses illegal shapes (you cannot send more than one card to a foundation), asks the right rule for the destination, splices the cards off one array, and pushes them onto another. If that uncovers a face-down card, it flips:

function move(fromId, idx, toId){
  const from = pile(fromId), to = pile(toId);
  if (fromId === toId || !from[idx] || !from[idx].faceUp) return false;
  const moving = from[idx], count = from.length - idx;

  let ok = false;
  if (toId[0] === "f")      ok = count === 1 && canDropFoundation(moving, toId);
  else if (toId[0] === "t") ok = isRun(fromId, idx) && canDropTableau(moving, toId);
  if (!ok) return false;

  to.push(...from.splice(idx));                          // move the slice
  if (fromId[0] === "t" && from.length && !top(from).faceUp)
    top(from).faceUp = true;                             // auto-flip what we uncovered
  settle(); render();
  return true;
}
Enter fullscreen mode Exit fullscreen mode

Because all the rules live here, click and drag can never disagree. Moving several cards at once is just moving a slice of one array onto another — allowed only when isRun confirms that slice is already a valid descending, alternating-colour sequence with no face-down card hiding inside it.

Winning is counting to 52

There is no clever end-state to track. After every move I add up the four foundations; when they hold all 52 cards, every suit has been built Ace to King and the game is won.

const home = foundations.reduce((n, f) => n + f.length, 0);
if (home === 52){ won = true; msg("You won! 🎉 All 52 cards home."); }
Enter fullscreen mode Exit fullscreen mode

That is the whole lesson of the build: keep the smallest possible source of truth — eleven arrays of card objects — and let everything else fall out of it. The blanks, the legal moves, the auto-flip, and the win test are all pure reads over that state.

Deal a game, drag a run across the tableau, or double-click a card to shoot it home:
https://dev48v.infy.uk/game/day35-solitaire.html

Top comments (0)