The deck below starts in perfect suit order: ♣ ♦ ♥ ♠, repeated thirteen times. Click Riffle Shuffle to split the deck roughly in half and interleave the two halves at random. Then click Check Groups to verify that every block of four consecutive cards still contains all four suits.
<!-- Gilbreath Principle demo — ordered deck, riffle shuffle, group check -->
<div class="controls">
<button id="btn-shuffle" type="button">Riffle Shuffle</button>
<button id="btn-check" type="button">Check Groups</button>
<button id="btn-reset" type="button" class="ghost">Reset Deck</button>
</div>
<div class="status" id="status"></div>
<div id="deck-view" class="deck-view"></div>
<div class="legend">
<span class="suit clubs">♣</span>
<span class="suit diamonds">♦</span>
<span class="suit hearts">♥</span>
<span class="suit spades">♠</span>
<span class="legend-label">Groups of 4 — each must contain all four suits</span>
</div>
/* base layout styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 14px system-ui; 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: default; }
/* status message styles */
.status { font-size: 1rem; font-weight: 600; min-height: 1.5em; margin-bottom: .5rem; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
/* deck grid and group border styles */
.deck-view { display: flex; flex-wrap: wrap; gap: 4px; }
.group { display: flex; gap: 2px; padding: 3px; border: 2px solid transparent;
border-radius: 8px; transition: border-color .3s; }
.group.ok { border-color: #0a7d33; }
.group.bad { border-color: #c92f3c; }
/* individual card styles */
.card { width: 32px; height: 44px; display: flex; align-items: center; justify-content: center;
font-size: 18px; border-radius: 6px; background: #f5f7fa;
border: 1px solid #cdd9e3; user-select: none; }
.card.clubs { color: #1d3557; }
.card.diamonds { color: #e63946; }
.card.hearts { color: #e63946; }
.card.spades { color: #1d3557; }
/* suit legend below the deck */
.legend { display: flex; align-items: center; gap: .4rem; margin-top: .6rem; font-size: .85rem; color: #555; }
.suit { font-size: 1.1rem; }
.suit.clubs, .suit.spades { color: #1d3557; }
.suit.diamonds, .suit.hearts { color: #e63946; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Suit names (logic keys) and Unicode glyphs for display */
const SUITS = ['clubs', 'diamonds', 'hearts', 'spades'];
const GLYPHS = { clubs: '♣', diamonds: '♦', hearts: '♥', spades: '♠' };
const K = 4; /* pattern length k = 4 suits */
/* Build the ordered deck — 13 repetitions of the 4-suit pattern */
function buildOrderedDeck() {
const deck = [];
for (let i = 0; i < 13; i++) {
for (const s of SUITS) deck.push(s);
}
return deck;
}
/* Riffle shuffle — deal off top cards (reversing them), then interleave with remainder */
function riffleShuffle(deck) {
const m = 1 + Math.floor(Math.random() * (deck.length - 1));
const left = deck.slice(0, m).reverse();
const right = deck.slice(m);
const result = [];
let l = 0, r = 0;
while (l < left.length && r < right.length) {
if (Math.random() < 0.5) result.push(left[l++]);
else result.push(right[r++]);
}
while (l < left.length) result.push(left[l++]);
while (r < right.length) result.push(right[r++]);
return result;
}
/* Check each group of k cards for all-suits completeness */
function checkGroups(deck) {
const groups = [];
for (let i = 0; i < deck.length; i += K) {
const group = deck.slice(i, i + K);
const suitSet = new Set(group);
groups.push({ cards: group, ok: suitSet.size === K && SUITS.every(s => suitSet.has(s)) });
}
return groups;
}
let currentDeck = buildOrderedDeck();
let shuffled = false;
function setStatus(text, cls) {
const el = document.getElementById('status');
el.textContent = text;
el.className = 'status ' + (cls || '');
}
/* Render the deck — plain (no border) or with group ok/bad borders */
function render(groups) {
const view = document.getElementById('deck-view');
view.innerHTML = '';
if (!groups) {
/* plain render before checking */
for (let i = 0; i < currentDeck.length; i += K) {
const g = document.createElement('div');
g.className = 'group';
for (const suit of currentDeck.slice(i, i + K)) {
const c = document.createElement('div');
c.className = 'card ' + suit;
c.textContent = GLYPHS[suit];
g.appendChild(c);
}
view.appendChild(g);
}
} else {
/* render with color-coded group borders */
for (const grp of groups) {
const g = document.createElement('div');
g.className = 'group ' + (grp.ok ? 'ok' : 'bad');
for (const suit of grp.cards) {
const c = document.createElement('div');
c.className = 'card ' + suit;
c.textContent = GLYPHS[suit];
g.appendChild(c);
}
view.appendChild(g);
}
}
}
document.getElementById('btn-shuffle').addEventListener('click', () => {
currentDeck = riffleShuffle(currentDeck);
shuffled = true;
render(null);
setStatus('Deck shuffled! Click Check Groups to verify the invariant.', '');
document.getElementById('btn-check').disabled = false;
});
document.getElementById('btn-check').addEventListener('click', () => {
const groups = checkGroups(currentDeck);
render(groups);
const allOk = groups.every(g => g.ok);
const badCount = groups.filter(g => !g.ok).length;
if (allOk) {
setStatus(i18nFmt('✓ All %n% groups are complete — the Gilbreath invariant holds!', { n: groups.length }), 'ok');
} else {
setStatus(i18nFmt('✗ %n% group(s) are incomplete — this should not happen with one riffle.', { n: badCount }), 'bad');
}
});
document.getElementById('btn-reset').addEventListener('click', () => {
currentDeck = buildOrderedDeck();
shuffled = false;
document.getElementById('btn-check').disabled = true;
render(null);
setStatus('Deck reset to ordered state.', '');
});
/* initialize with ordered deck and disabled check button */
document.getElementById('btn-check').disabled = true;
setStatus('Deck is in order (♣ ♦ ♥ ♠ × 13). Click Riffle Shuffle.', '');
render(null);
<!-- Gilbreath Principle demo — ordered deck, riffle shuffle, group check -->
<div class="controls">
<button id="btn-shuffle" type="button">Riffle Shuffle</button>
<button id="btn-check" type="button">Check Groups</button>
<button id="btn-reset" type="button" class="ghost">Reset Deck</button>
</div>
<div class="status" id="status"></div>
<div id="deck-view" class="deck-view"></div>
<div class="legend">
<span class="suit clubs">♣</span>
<span class="suit diamonds">♦</span>
<span class="suit hearts">♥</span>
<span class="suit spades">♠</span>
<span class="legend-label">Groups of 4 — each must contain all four suits</span>
</div>
/* base layout styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 14px system-ui; 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: default; }
/* status message styles */
.status { font-size: 1rem; font-weight: 600; min-height: 1.5em; margin-bottom: .5rem; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
/* deck grid and group border styles */
.deck-view { display: flex; flex-wrap: wrap; gap: 4px; }
.group { display: flex; gap: 2px; padding: 3px; border: 2px solid transparent;
border-radius: 8px; transition: border-color .3s; }
.group.ok { border-color: #0a7d33; }
.group.bad { border-color: #c92f3c; }
/* individual card styles */
.card { width: 32px; height: 44px; display: flex; align-items: center; justify-content: center;
font-size: 18px; border-radius: 6px; background: #f5f7fa;
border: 1px solid #cdd9e3; user-select: none; }
.card.clubs { color: #1d3557; }
.card.diamonds { color: #e63946; }
.card.hearts { color: #e63946; }
.card.spades { color: #1d3557; }
/* suit legend below the deck */
.legend { display: flex; align-items: center; gap: .4rem; margin-top: .6rem; font-size: .85rem; color: #555; }
.suit { font-size: 1.1rem; }
.suit.clubs, .suit.spades { color: #1d3557; }
.suit.diamonds, .suit.hearts { color: #e63946; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Suit names (logic keys) and Unicode glyphs for display */
const SUITS = ['clubs', 'diamonds', 'hearts', 'spades'];
const GLYPHS = { clubs: '♣', diamonds: '♦', hearts: '♥', spades: '♠' };
const K = 4; /* pattern length k = 4 suits */
/* Build the ordered deck — 13 repetitions of the 4-suit pattern */
function buildOrderedDeck() {
const deck = [];
for (let i = 0; i < 13; i++) {
for (const s of SUITS) deck.push(s);
}
return deck;
}
/* Riffle shuffle — deal off top cards (reversing them), then interleave with remainder */
function riffleShuffle(deck) {
const m = 1 + Math.floor(Math.random() * (deck.length - 1));
const left = deck.slice(0, m).reverse();
const right = deck.slice(m);
const result = [];
let l = 0, r = 0;
while (l < left.length && r < right.length) {
if (Math.random() < 0.5) result.push(left[l++]);
else result.push(right[r++]);
}
while (l < left.length) result.push(left[l++]);
while (r < right.length) result.push(right[r++]);
return result;
}
/* Check each group of k cards for all-suits completeness */
function checkGroups(deck) {
const groups = [];
for (let i = 0; i < deck.length; i += K) {
const group = deck.slice(i, i + K);
const suitSet = new Set(group);
groups.push({ cards: group, ok: suitSet.size === K && SUITS.every(s => suitSet.has(s)) });
}
return groups;
}
let currentDeck = buildOrderedDeck();
let shuffled = false;
function setStatus(text, cls) {
const el = document.getElementById('status');
el.textContent = text;
el.className = 'status ' + (cls || '');
}
/* Render the deck — plain (no border) or with group ok/bad borders */
function render(groups) {
const view = document.getElementById('deck-view');
view.innerHTML = '';
if (!groups) {
/* plain render before checking */
for (let i = 0; i < currentDeck.length; i += K) {
const g = document.createElement('div');
g.className = 'group';
for (const suit of currentDeck.slice(i, i + K)) {
const c = document.createElement('div');
c.className = 'card ' + suit;
c.textContent = GLYPHS[suit];
g.appendChild(c);
}
view.appendChild(g);
}
} else {
/* render with color-coded group borders */
for (const grp of groups) {
const g = document.createElement('div');
g.className = 'group ' + (grp.ok ? 'ok' : 'bad');
for (const suit of grp.cards) {
const c = document.createElement('div');
c.className = 'card ' + suit;
c.textContent = GLYPHS[suit];
g.appendChild(c);
}
view.appendChild(g);
}
}
}
document.getElementById('btn-shuffle').addEventListener('click', () => {
currentDeck = riffleShuffle(currentDeck);
shuffled = true;
render(null);
setStatus('Deck shuffled! Click Check Groups to verify the invariant.', '');
document.getElementById('btn-check').disabled = false;
});
document.getElementById('btn-check').addEventListener('click', () => {
const groups = checkGroups(currentDeck);
render(groups);
const allOk = groups.every(g => g.ok);
const badCount = groups.filter(g => !g.ok).length;
if (allOk) {
setStatus(i18nFmt('✓ All %n% groups are complete — the Gilbreath invariant holds!', { n: groups.length }), 'ok');
} else {
setStatus(i18nFmt('✗ %n% group(s) are incomplete — this should not happen with one riffle.', { n: badCount }), 'bad');
}
});
document.getElementById('btn-reset').addEventListener('click', () => {
currentDeck = buildOrderedDeck();
shuffled = false;
document.getElementById('btn-check').disabled = true;
render(null);
setStatus('Deck reset to ordered state.', '');
});
/* initialize with ordered deck and disabled check button */
document.getElementById('btn-check').disabled = true;
setStatus('Deck is in order (♣ ♦ ♥ ♠ × 13). Click Riffle Shuffle.', '');
render(null);
No matter how you shuffle, the groups stay complete. That is the Gilbreath invariant in action. Notice the order within each group is scrambled — the guarantee is only that all four suits appear, not which order they arrive in. Shuffle again and again: the invariant holds every time.
Comments
Loading comments...