Introduction

Take a deck of cards and split them into piles of any sizes you like. Now apply one rule, over and over: take exactly one card from every existing pile, collect those cards into a brand-new pile, and repeat. That is Bulgarian Solitaire.

The rule looks harmless. But something astonishing happens when the total number of cards is a triangular number — a number of the form Tk=1+2+⋯+k=k(k+1)2T_k = 1 + 2 + \dots + k = \frac{k(k+1)}{2} for some positive integer kk. No matter how you start, the piles will eventually reach the configuration {1,2,3,…,k}\{1, 2, 3, \dots, k\} — a perfect staircase — and stay there forever.

The game was popularized in the early 1980s, notably by the mathematician Martin Gardner in his Scientific American column (1983), and the convergence theorem was proved around the same time by Ethan Akin and Morton Davis (1985) and, independently, by several others. It is a rare example of a completely solved discrete dynamical system: no chaos, no cycles — just inevitable order.

Watch the Staircase Form

Set the starting piles below — each number is the size of one pile. The total must be a triangular number (Tk=k(k+1)/2T_k = k(k+1)/2 for some kk). Press Step to apply one round of the rule, or Run to animate until the staircase appears.

<!-- Bulgarian Solitaire demo — pile visualization + step-by-step animation -->
<div class="controls">
  <label for="preset-select">Starting piles:</label>
  <select id="preset-select">
    <option value="6,4,1">6, 4, 1 (T₅ = 15)</option>
    <option value="4,3,2,1">4, 3, 2, 1 (T₄ = 10)</option>
    <option value="14,1">14, 1 (T₅ = 15)</option>
    <option value="custom">Custom…</option>
  </select>
  <input id="custom-input" type="text" placeholder="e.g. 3 2 1 or 4,2" style="display:none" />
</div>
<div id="piles-display" class="piles-display" aria-label="Pile visualization"></div>
<div id="status-bar" class="status-bar"></div>
<div class="btns">
  <button id="btn-step" type="button">Step</button>
  <button id="btn-run" type="button">Run</button>
  <button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="step-count" class="step-count"></div>
/* Layout: column-per-pile, cards stacked bottom-to-top; new pile highlighted in red */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 4px; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin-bottom: .6rem; font-size: .88rem; }
label { font-weight: 600; }
select, input[type=text] { font: .88rem system-ui, sans-serif; padding: .3rem .5rem;
  border: 1px solid #adb1b8; border-radius: 6px; background: #f5f7fa; }
.piles-display { display: flex; align-items: flex-end; gap: 6px; min-height: 140px;
  border-bottom: 2px solid #c0c8d2; padding-bottom: 4px; margin-bottom: .5rem;
  flex-wrap: wrap; }
.pile { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.card { width: 28px; height: 18px; border-radius: 3px; background: #4a90d9; border: 1px solid #2f6db5; }
.card.new-pile { background: #e63946; border-color: #c92f3c; }
.pile-label { font-size: .7rem; font-weight: 700; color: #555; margin-top: 2px; }
.status-bar { font-size: .92rem; font-weight: 600; min-height: 1.4em; margin: .3rem 0; }
.status-bar.done { color: #0a7d33; }
.status-bar.running { color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
  background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: not-allowed; }
.step-count { font-size: .82rem; color: #666; margin-top: .4rem; }
.target-note { font-size: .78rem; color: #888; margin-top: .3rem; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Bulgarian Solitaire: one round = take 1 card from every pile, form new pile of that count.
// The rule always terminates at the staircase {1,2,...,k} when total = T_k = k(k+1)/2.

// triangularRoot(n): returns k if n = T_k, else -1
function triangularRoot(n) {
  const k = Math.round((-1 + Math.sqrt(1 + 8 * n)) / 2);
  return k * (k + 1) / 2 === n ? k : -1;
}

// staircase(k): target configuration [1, 2, ..., k]
function staircase(k) {
  const t = [];
  for (let i = 1; i <= k; i++) t.push(i);
  return t;
}

// isDone: compare sorted piles to the staircase
function isDone(piles, k) {
  const s = staircase(k);
  const sorted = [...piles].sort((a, b) => a - b);
  return sorted.length === s.length && sorted.every((v, i) => v === s[i]);
}

// applyStep: shrink every pile by 1 (drop zeros), add new pile of size = old pile count
function applyStep(piles) {
  const newPile = piles.length; // new pile gets one card from each of the current piles
  const next = piles.map(p => p - 1).filter(p => p > 0);
  next.push(newPile);
  return next;
}

let piles = [];
let k = 0;
let stepCount = 0;
let running = false;
let animTimer = null;
let lastNewPileIndex = -1;

const displayEl = document.getElementById('piles-display');
const statusEl = document.getElementById('status-bar');
const stepCountEl = document.getElementById('step-count');
const btnStep = document.getElementById('btn-step');
const btnRun = document.getElementById('btn-run');
const btnReset = document.getElementById('btn-reset');
const presetSel = document.getElementById('preset-select');
const customInput = document.getElementById('custom-input');

function parsePiles(str) {
  return str.split(/[\s,]+/).map(Number).filter(n => Number.isInteger(n) && n > 0);
}

function renderPiles(newIdx) {
  displayEl.innerHTML = '';
  piles.forEach((size, idx) => {
    const col = document.createElement('div');
    col.className = 'pile';
    const isNew = idx === newIdx;
    for (let i = 0; i < size; i++) {
      const card = document.createElement('div');
      card.className = 'card' + (isNew ? ' new-pile' : '');
      col.appendChild(card);
    }
    const lbl = document.createElement('div');
    lbl.className = 'pile-label';
    lbl.textContent = String(size);
    col.appendChild(lbl);
    displayEl.appendChild(col);
  });
}

function setStatus(msg, cls) {
  statusEl.textContent = msg;
  statusEl.className = 'status-bar ' + (cls || '');
}

function updateStepCount() {
  if (stepCount === 0) {
    stepCountEl.textContent = '';
  } else {
    stepCountEl.textContent = i18nFmt('Steps so far: %n%', { n: stepCount });
  }
}

function load(pilesArr) {
  stopRun();
  piles = pilesArr.slice();
  stepCount = 0;
  lastNewPileIndex = -1;
  const total = piles.reduce((a, b) => a + b, 0);
  k = triangularRoot(total);
  renderPiles(-1);
  updateStepCount();
  if (k < 0) {
    setStatus(i18nFmt('Total %n% is not a triangular number — try a preset or a custom sum like 10, 15, or 21.', { n: total }), 'running');
    btnStep.disabled = true;
    btnRun.disabled = true;
  } else if (isDone(piles, k)) {
    setStatus('Already at the staircase! The fixed point has been reached.', 'done');
    btnStep.disabled = true;
    btnRun.disabled = true;
  } else {
    setStatus(i18nFmt('Total = %total% = T_%k% · Press Step or Run to watch convergence.', { total: total, k: k }), 'running');
    btnStep.disabled = false;
    btnRun.disabled = false;
  }
}

function doStep() {
  if (isDone(piles, k)) return;
  const sorted = [...piles].sort((a, b) => a - b);
  const next = applyStep(sorted);
  const nextSorted = [...next].sort((a, b) => a - b);
  // in sorted order the new pile (largest) lands last
  const newPileIdx = nextSorted.length - 1;
  piles = nextSorted;
  stepCount++;
  lastNewPileIndex = newPileIdx;
  renderPiles(newPileIdx);
  updateStepCount();
  if (isDone(piles, k)) {
    setStatus(i18nFmt('Staircase reached in %steps% step(s)!', { steps: stepCount }), 'done');
    btnStep.disabled = true;
    btnRun.disabled = true;
    stopRun();
  } else {
    setStatus(i18nFmt('Step done — %n% pile(s) now.', { n: piles.length }), 'running');
  }
}

function stopRun() {
  running = false;
  if (animTimer) { clearTimeout(animTimer); animTimer = null; }
  btnRun.textContent = 'Run';
  btnStep.disabled = piles.length === 0 || k < 0 || isDone(piles, k);
}

function startRun() {
  running = true;
  btnRun.textContent = 'Stop';
  btnStep.disabled = true;
  function tick() {
    if (!running || isDone(piles, k)) { stopRun(); return; }
    doStep();
    animTimer = setTimeout(tick, 320);
  }
  tick();
}

btnStep.addEventListener('click', () => { if (!running) doStep(); });
btnRun.addEventListener('click', () => { if (running) stopRun(); else startRun(); });
btnReset.addEventListener('click', () => {
  stopRun();
  const val = presetSel.value;
  if (val === 'custom') {
    const p = parsePiles(customInput.value);
    if (p.length > 0) load(p); else load([6, 4, 1]);
  } else {
    load(parsePiles(val));
  }
});

presetSel.addEventListener('change', () => {
  stopRun();
  if (presetSel.value === 'custom') {
    customInput.style.display = '';
    customInput.focus();
  } else {
    customInput.style.display = 'none';
    load(parsePiles(presetSel.value));
  }
});

customInput.addEventListener('change', () => {
  const p = parsePiles(customInput.value);
  if (p.length > 0) load(p);
});

// Initialize with default preset
load([6, 4, 1]);
Result
Console

Notice that the pile sizes jump around erratically at first, then gradually sort themselves out. The distance to the fixed point — measured as the sum of absolute differences from the target staircase — never increases on average, and must eventually reach zero. That monotonic decrease is the engine of the proof.

Why It Always Converges

The proof that Bulgarian Solitaire always reaches the staircase is elegant and elementary.

The fixed point. When the total is Tk=k(k+1)/2T_k = k(k+1)/2, the only configuration the rule maps to itself is the staircase {1,2,3,…,k}\{1, 2, 3, \dots, k\}. Applying the rule to the staircase takes one card from each of kk piles to form a pile of size kk, and each remaining pile shrinks by one — giving {0,1,2,…,k−1}∪{k}={1,2,…,k}\{0, 1, 2, \dots, k-1\} \cup \{k\} = \{1, 2, \dots, k\} again (dropping the empty pile of size 0). The staircase is self-reproducing.

The monovariant. Define a score for any configuration as Φ=∑ipi2\Phi = \sum_{i} p_i^2 (the sum of squares of pile sizes). One can show that Φ\Phi either decreases or stays the same after each step, and it can only stay the same when the configuration is already the staircase. Since Φ\Phi is a non-negative integer, it must eventually stop decreasing — and that stopping point is the fixed point.

The bound. Starting from any configuration with TkT_k cards, the staircase is always reached in at most k(k−1)k(k-1) steps. For k=5k = 5 that is at most 20 steps; for k=10k = 10, at most 90. The convergence is fast and guaranteed.

Non-triangular totals behave differently: instead of a single fixed point the system falls into a cycle of configurations that keep repeating. The triangular-number condition is not an accident — it is precisely what ensures a fixed point exists.

This makes Bulgarian Solitaire a textbook example of a completely solved dynamical system: unlike the Collatz conjecture, there is no mystery about what happens in the long run.

Where It Matters

Bulgarian Solitaire is small, but the ideas it showcases appear throughout mathematics and computer science:

  • Monovariants and termination proofs: the same technique — find a quantity that always decreases toward a bound — is how we prove that algorithms terminate. Loop invariants in dynamic programming, decreasing potential functions in network protocols, and well-founded orderings in theorem provers all work the same way.
  • Discrete dynamical systems: Bulgarian Solitaire is a simple model of a system that self-organizes. Similar convergence phenomena appear in cellular automata, sorting networks, and chip-firing games on graphs.
  • Combinatorics and partition theory: the piles are integer partitions of TkT_k. The study of how partition operations behave is a central topic in combinatorics, with connections to Young tableaux, representation theory, and symmetric functions.
  • Teaching fixed-point arguments: because the rule is so concrete, Bulgarian Solitaire is a favorite example for introducing students to fixed-point theorems — the idea that repeated application of a rule must eventually stop at a self-reproducing state.

Simple rules with inevitable outcomes: that is a pattern worth recognizing wherever you find it.

Conclusion

Bulgarian Solitaire is an almost absurdly simple process — take one card from each pile, make a new pile — yet it hides a theorem that feels like magic: from any starting arrangement, provided the total is a triangular number, the piles will march inexorably toward the perfect staircase {1,2,…,k}\{1, 2, \dots, k\} and never leave.

The key is the monovariant: a quantity that the rule is obliged to decrease, step after step, until there is nowhere left to go but the fixed point. That same logic — find what must decrease, conclude that it must stop — underlies termination proofs across all of algorithm design.

So the next time you wonder why a loop has to end, or why an iterative process has to converge, think of the card piles patiently sorting themselves into a staircase. Order does not always emerge from chaos — but when a monovariant is watching, it has no other choice.

Share this article

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

Comments

Loading comments...

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