Introduction

For decades, FreeCell shipped with Windows and built a reputation as the solitaire you can almost always win. All 52 cards lie face-up from the start, you have four spare free cells to park a card each, and the goal is simple: build the four suits up from Ace to King on the foundations.

Because every card is visible, it feels like pure logic — no hidden information, no luck of the draw. And famously, of the original Microsoft set of 32,000 numbered deals, every one was found to be winnable except deal #11,982.

So is FreeCell easy? When you generalize it to n cards instead of 52, the answer flips. Deciding whether a deal can be solved at all turns out to be one of the hardest problems we know — the same wall that separates the easy problems from the intractable ones in all of computer science.

Play the Deal

Here is a miniature FreeCell deal: a few cards in two cascades, two free cells, and one foundation that must fill A, 2, 3, … in order. Click a card to pick it up, then click a destination to drop it. Send every card to the foundation to win.

<p class="hint">A miniature FreeCell deal. The <b>foundation</b> must fill A, 2, 3, 4, 5
in order. You have <b>2 free cells</b> (park one card each) and <b>2 cascades</b>
(only the bottom card moves). Click a card to pick it up, then click where it goes.
Clear every card to win.</p>
<div class="zones">
  <div class="row">
    <div class="slot free" data-z="F0"></div>
    <div class="slot free" data-z="F1"></div>
    <div class="gap"></div>
    <div class="slot found" data-z="H"></div>
  </div>
  <div class="row casc">
    <div class="col" data-z="C0"></div>
    <div class="col" data-z="C1"></div>
  </div>
</div>
<div class="status" id="status">Pick a card to move.</div>
<div class="btns">
  <button id="solve" type="button">Auto-solve</button>
  <button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="tree-wrap">
  <div class="tree-title">Search tree the solver explores</div>
  <div id="tree" class="tree"></div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.zones { background: #11572b; padding: 10px; border-radius: 10px; }
.row { display: flex; gap: 8px; align-items: flex-start; margin-bottom: 10px; }
.row:last-child { margin-bottom: 0; }
.gap { flex: 1; }
.slot, .col { border-radius: 7px; min-width: 44px; }
.slot { width: 44px; height: 60px; border: 2px dashed #2f7d4c; display: flex;
        align-items: center; justify-content: center; }
.found { border-color: #ffd76a; }
.col { min-height: 60px; padding: 3px; border: 2px dashed #2f7d4c;
       display: flex; flex-direction: column; gap: 3px; }
.card { width: 40px; height: 54px; border-radius: 6px; background: #fff;
        border: 1px solid #888; font: 700 16px ui-monospace, monospace;
        display: flex; align-items: center; justify-content: center; cursor: pointer;
        user-select: none; }
.card.red { color: #c92f3c; }
.card.black { color: #1d2733; }
.col .card { width: 38px; }
.card.sel { outline: 3px solid #ffd76a; outline-offset: 1px; }
.slot.drop, .col.drop { border-style: solid; border-color: #ffd76a; }
.status { font-size: 1rem; font-weight: 600; margin: .6rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #11572b;
         background: #11572b; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #11572b; }
.tree-wrap { margin-top: 1rem; }
.tree-title { font-size: .8rem; color: #555; margin-bottom: .3rem; font-weight: 600; }
.tree { font: 12px/1.5 ui-monospace, monospace; white-space: pre; overflow-x: auto;
        background: #f5f7f9; border: 1px solid #dde3e8; border-radius: 8px; padding: 8px;
        max-height: 220px; min-height: 40px; color: #2b3b4b; }
.tree .win { color: #0a7d33; font-weight: 700; }
.tree .dead { color: #b14; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Miniature FreeCell. Foundation builds A(1)..5 in order. Two free cells, two
    // cascades; only the bottom card of a cascade is movable. Solving FreeCell in
    // general (n cards) is NP-complete (Helmert, 2003): the auto-solver below does a
    // depth-first search with backtracking over the tree of move sequences.
const GOAL = 5; // need ranks 1..5 on the foundation
const RED = new Set(['h','d']);
// Deal: each card = {r: rank, s: suit}. Only ranks 1..5, suit 's' (one suit ⇒ foundation needs 1..5).
// A solvable layout that needs free cells + a cascade-to-cascade move.
function initState(){
  return {
    free: [null, null],
    cols: [ [{r:3,s:'s'},{r:1,s:'s'},{r:4,s:'s'}], [{r:5,s:'s'},{r:2,s:'s'}] ],
    found: 0 // highest rank placed (0 = empty, then 1..5)
  };
}
let S = initState();
let sel = null; // {z:'C0'|'C1'|'F0'|'F1'}

const label = c => c ? (c.r===1?'A':c.r) + c.s : '';
function topOf(zone){
  if (zone[0]==='C'){ const col=S.cols[+zone[1]]; return col.length? col[col.length-1] : null; }
  if (zone[0]==='F'){ return S.free[+zone[1]]; }
  return null;
}
// Can we move the card currently at `from` onto zone `to`?
function canMove(from, to){
  const card = topOf(from);
  if (!card) return false;
  if (to[0]==='H') return card.r === S.found + 1;           // foundation: next rank up
  if (to[0]==='F') return S.free[+to[1]] === null;          // free cell: must be empty
  if (to[0]==='C') return S.cols[+to[1]].length === 0;      // cascade: only onto empty col here (simplified)
  return false;
}
function applyMove(from, to){
  let card;
  if (from[0]==='C') card = S.cols[+from[1]].pop();
  else { card = S.free[+from[1]]; S.free[+from[1]] = null; }
  if (to[0]==='H') S.found = card.r;
  else if (to[0]==='F') S.free[+to[1]] = card;
  else S.cols[+to[1]].push(card);
}
const won = () => S.found === GOAL;

function render(){
  document.querySelectorAll('.slot,.col').forEach(el=>{
    const z = el.dataset.z;
    el.classList.remove('drop');
    if (z[0]==='F'){
      el.innerHTML=''; const c=S.free[+z[1]];
      if (c) el.appendChild(cardEl(c, z));
    } else if (z==='H'){
      el.innerHTML = S.found? '<div class="card '+(false?'':'black')+'">'+(S.found===1?'A':S.found)+'s</div>' : '';
    } else {
      el.innerHTML=''; S.cols[+z[1]].forEach((c,i,arr)=>{
        const e=cardEl(c, z); if (i!==arr.length-1) e.style.cursor='default'; el.appendChild(e);
      });
    }
    if (sel && canMove(sel.z, z)) el.classList.add('drop');
  });
}
function cardEl(c, zone){
  const d=document.createElement('div');
  d.className='card '+(RED.has(c.s)?'red':'black');
  d.textContent=label(c);
  if (sel && sel.z===zone && topOf(zone)===c) d.classList.add('sel');
  return d;
}
function setStatus(t,cls){ const s=document.getElementById('status'); s.textContent=t; s.className='status '+(cls||''); }

function onZoneClick(z){
  if (won()) return;
  if (!sel){
    if (topOf(z)){ sel={z}; render(); setStatus('Now click a destination.'); }
    return;
  }
  if (sel.z===z){ sel=null; render(); setStatus('Pick a card to move.'); return; }
  if (canMove(sel.z, z)){
    applyMove(sel.z, z); sel=null;
    if (won()){ render(); setStatus('You cleared the deal! ✓','ok'); }
    else { render(); setStatus('Keep going.'); }
  } else {
    setStatus('That move is not legal.','bad');
  }
}
document.querySelectorAll('.slot,.col').forEach(el=>{
  el.addEventListener('click', ()=> onZoneClick(el.dataset.z));
});

// ── Auto-solver: depth-first search with backtracking; logs the tree ──────────
const ZONES = ['H','F0','F1','C0','C1'];
function clone(s){ return { free:[s.free[0],s.free[1]], cols:[s.cols[0].slice(),s.cols[1].slice()], found:s.found }; }
function legalMoves(s){
  const cur=S; S=s; const out=[];
  for (const from of ['C0','C1','F0','F1']) for (const to of ZONES){
    if (from===to) continue;
    if (canMove(from,to)){
      // avoid pointless free<->free shuffles by only allowing free->free never
      if (from[0]==='F' && to[0]==='F') continue;
      out.push([from,to]);
    }
  }
  S=cur; return out;
}
function doMove(s, mv){ const cur=S; S=s; applyMove(mv[0],mv[1]); S=cur; }
function isWin(s){ return s.found===GOAL; }

function solve(start){
  const lines=[]; let nodes=0; const seen=new Set();
  const key=s=>JSON.stringify(s);
  let solution=null;
  function dfs(s, depth, prefix){
    nodes++;
    const k=key(s);
    if (seen.has(k)){ lines.push(fmt('%prefix%· (repeat)', { prefix: prefix })); return false; }
    seen.add(k);
    if (isWin(s)){ lines.push(fmt('<span class="win">%prefix%✔ WIN</span>', { prefix: prefix })); solution=true; return true; }
    if (depth>14){ lines.push(fmt('<span class="dead">%prefix%✗ too deep</span>', { prefix: prefix })); return false; }
    const moves=legalMoves(s);
    if (moves.length===0){ lines.push(fmt('<span class="dead">%prefix%✗ dead end</span>', { prefix: prefix })); return false; }
    for (const mv of moves){
      const ns=clone(s); doMove(ns,mv);
      lines.push(prefix+mv[0]+'→'+mv[1]);
      if (dfs(ns, depth+1, prefix+'  ')) return true;
    }
    return false;
  }
  dfs(clone(start), 0, '');
  return { ok:!!solution, nodes, lines };
}

function autoSolve(){
  S = initState(); sel=null; render();
  const res = solve(S);
  const tree=document.getElementById('tree');
  tree.innerHTML = res.lines.join('\n');
  if (res.ok){ setStatus(fmt('Solver found a winning line after exploring %nodes% positions.', { nodes: res.nodes }),'ok');
    console.log('FreeCell solved — explored', res.nodes, 'positions'); }
  else setStatus('No solution found.','bad');
}
document.getElementById('solve').addEventListener('click', autoSolve);
document.getElementById('reset').addEventListener('click', ()=>{ S=initState(); sel=null; render(); document.getElementById('tree').innerHTML=''; setStatus('Pick a card to move.'); });
render();
Result
Console

Notice the asymmetry. Checking a finished game of moves is effortless: replay the moves and confirm each one is legal. Finding the right sequence is the hard part — press Auto-solve and the computer searches a branching tree of move sequences, backtracking whenever it hits a dead end. Each extra card can multiply the number of paths it must explore.

The Real Complexity

How hard is FreeCell, really? Not playing a single 52-card deal — solving the game in general.

  • Checking a proposed solution is trivial: replay the listed moves and confirm each is legal and the foundations end full.
  • Search explores a tree of move sequences. From any position several moves are legal; each choice opens a new branch, and the tree can grow exponentially with the number of cards.
  • It's NP-complete. In 2003 the researcher Malte Helmert proved that generalized FreeCell — the game scaled to n cards — is NP-complete. He built gadgets out of cards so that any SAT-style problem could be encoded as a deal that is solvable if and only if the underlying problem has a solution.
  • So deciding even is-this-deal-solvable is equivalent to the whole NP-complete family — yet the standard 52-card deals are tiny and nearly all winnable, which is why FreeCell feels easy.

That is the punchline: the comfortable solitaire and the intractable problem are the same game at different sizes. The gap between "obviously winnable" and "no one can tell" is exactly the gap behind P vs NP.

Where It Matters

"Find a legal sequence of moves that reaches a goal" is one of the most common shapes a real problem can take, and FreeCell is its friendly face:

  • Automated planning: AI planners that schedule robots, factories or spacecraft search the same kind of move tree — FreeCell is a classic benchmark in the planning community.
  • Logistics and routing: loading, stacking and reshuffling containers is FreeCell with bigger cards.
  • Puzzle and game solvers: solitaire solvers, Sokoban and Rush Hour all rely on the same search-and-backtrack engine.
  • Teaching complexity: because everyone has played a patience game, FreeCell is a vivid on-ramp to what NP-completeness even means.

Learn why FreeCell is hard and you've met state-space search — the engine under planners, SAT solvers and countless scheduling problems.

Conclusion

FreeCell hides a beautiful secret: the same cards that make the game feel so winnable can be arranged into gadgets that encode any problem in NP. Checking a solution stays instant; deciding whether a general deal can be solved at all is as hard as anything in computer science.

So the next time a FreeCell deal yields gracefully to a few clicks, enjoy it — you are playing the easy, friendly corner of a problem that, at scale, runs straight into P vs NP, with no known shortcut around the search.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/freecell/Content licensed under CC BY-NC 4.0.