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
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];
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
}
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;
}
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."); }
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)