Below is a small pool of possible answers. Click a guess and see how much information it carries. Each guess sorts the words into groups by the feedback they would produce; a guess that splits them evenly carries more bits and leaves fewer words standing.
<p class="hint">The secret is one of these <b id="count">20</b> words. A guess splits them
into groups by the green/yellow/gray feedback it would give. The best guess is the one
that splits most evenly — it carries the most <b>information</b> (bits) and shrinks the
list fastest. Pick a guess and watch how much it prunes.</p>
<div class="guesses" id="guesses"></div>
<div class="readout">
<div class="bar"><div class="fill" id="fill"></div></div>
<div class="stat" id="stat">Choose a guess to score it.</div>
</div>
<div class="pool" id="pool"></div>
<div class="btns">
<button id="best" type="button">Show the best opener</button>
<button id="worst" type="button" class="ghost">Show the worst</button>
</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; }
.guesses { display: flex; flex-wrap: wrap; gap: .4rem; margin: .4rem 0 .8rem; }
.guess { font: 700 14px ui-monospace, monospace; letter-spacing: 1px; padding: .4rem .6rem;
border: 1px solid #cdd9e3; background: #e8eef3; color: #1d3557; border-radius: 8px;
cursor: pointer; transition: all .1s; }
.guess:hover { background: #d7e2ec; }
.guess.sel { background: #1d3557; color: #fff; border-color: #1d3557; }
.readout { margin: .3rem 0 .8rem; }
.bar { height: 14px; background: #e6e8eb; border-radius: 7px; overflow: hidden; }
.fill { height: 100%; width: 0; background: #2a9d8f; transition: width .35s ease; }
.stat { font-size: .95rem; font-weight: 600; margin-top: .45rem; min-height: 1.3em; color: #1d3557; }
.pool { display: grid; grid-template-columns: repeat(5, 1fr); gap: 4px; margin: .3rem 0 .8rem; }
.w { font: 600 12px ui-monospace, monospace; text-align: center; padding: .35rem 0; border-radius: 6px;
background: #eef1f4; color: #555; transition: all .25s; }
.w.live { background: #d8f3ec; color: #0a6b5c; }
.w.dead { opacity: .28; text-decoration: line-through; }
.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; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Wordle as an INFORMATION problem. The secret is one of WORDS. Each guess, scored
// against a hidden secret, returns a colored pattern (green/yellow/gray). A guess
// partitions the candidate list into one bucket per possible pattern. The expected
// information gained is the ENTROPY of that bucket distribution, in bits:
// H = -Σ p(pattern) · log2 p(pattern).
// A high-entropy guess splits the words evenly, so on average it leaves far fewer
// candidates. This is exactly how optimal Wordle bots choose their openers.
const WORDS = ["CRANE","SLATE","TRACE","PLANT","BRICK","GHOST","JUMPY","VIVID","FUZZY","NYMPH","CRANK","SHINE","ROAST","DROWN","FLUTE","GRIME","BLOND","QUART","WALTZ","CHOMP"];
const GUESSES = ["CRANE","SLATE","ROAST","NYMPH","FUZZY","VIVID"];
// Standard Wordle feedback (two-pass, handles repeated letters): 2=green,1=yellow,0=gray.
function pattern(guess, secret){
const res=[0,0,0,0,0], sc=secret.split("");
for(let i=0;i<5;i++){ if(guess[i]===secret[i]){ res[i]=2; sc[i]=null; } }
for(let i=0;i<5;i++){ if(res[i]===0){ const j=sc.indexOf(guess[i]); if(j>=0){ res[i]=1; sc[j]=null; } } }
return res.join("");
}
// Expected information (bits) of a guess over the candidate set.
function entropy(guess, cands){
const b={}; for(const s of cands){ const p=pattern(guess,s); b[p]=(b[p]||0)+1; }
let H=0; const n=cands.length;
for(const k in b){ const pr=b[k]/n; H -= pr*Math.log2(pr); } return H;
}
// Average number of candidates left after this guess (over all possible secrets).
function avgRemaining(guess, cands){
const b={}; for(const s of cands){ const p=pattern(guess,s); b[p]=(b[p]||0)+1; }
let s=0; for(const k in b){ s += b[k]*b[k]; } return s/cands.length;
}
let selected=null;
const $=id=>document.getElementById(id);
function drawGuesses(){
const g=$("guesses"); g.innerHTML="";
for(const w of GUESSES){
const d=document.createElement("div");
d.className="guess"+(w===selected?" sel":""); d.textContent=w;
d.addEventListener("click",()=>select(w)); g.appendChild(d);
}
}
function drawPool(live){
const p=$("pool"); p.innerHTML="";
for(const w of WORDS){
const d=document.createElement("div");
d.className="w"+(live?(live.has(w)?" live":" dead"):"");
d.textContent=w; p.appendChild(d);
}
}
function select(w){
selected=w; drawGuesses();
const H=entropy(w,WORDS), avg=avgRemaining(w,WORDS), maxH=Math.log2(WORDS.length);
$("fill").style.width=(100*H/maxH).toFixed(1)+"%";
// Show one concrete split: pretend the secret is the first word and keep its bucket.
const fb=pattern(w,WORDS[0]);
const live=new Set(WORDS.filter(x=>pattern(w,x)===fb));
drawPool(live);
$("stat").textContent=fmt('“%w%”” carries %H% bits — on average it leaves %avg% of %length% words. (Example split shown: %size% left.)', { w: w, H: H.toFixed(2), avg: avg.toFixed(1), length: WORDS.length, size: live.size });
}
$("best").addEventListener("click",()=>{
let b=GUESSES[0]; for(const w of GUESSES) if(entropy(w,WORDS)>entropy(b,WORDS)) b=w; select(b);
});
$("worst").addEventListener("click",()=>{
let b=GUESSES[0]; for(const w of GUESSES) if(entropy(w,WORDS)<entropy(b,WORDS)) b=w; select(b);
});
drawGuesses(); drawPool(null); $("count").textContent=WORDS.length;
console.log("Entropy of each guess (bits):");
for(const w of GUESSES) console.log(w, entropy(w,WORDS).toFixed(3));
<p class="hint">The secret is one of these <b id="count">20</b> words. A guess splits them
into groups by the green/yellow/gray feedback it would give. The best guess is the one
that splits most evenly — it carries the most <b>information</b> (bits) and shrinks the
list fastest. Pick a guess and watch how much it prunes.</p>
<div class="guesses" id="guesses"></div>
<div class="readout">
<div class="bar"><div class="fill" id="fill"></div></div>
<div class="stat" id="stat">Choose a guess to score it.</div>
</div>
<div class="pool" id="pool"></div>
<div class="btns">
<button id="best" type="button">Show the best opener</button>
<button id="worst" type="button" class="ghost">Show the worst</button>
</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; }
.guesses { display: flex; flex-wrap: wrap; gap: .4rem; margin: .4rem 0 .8rem; }
.guess { font: 700 14px ui-monospace, monospace; letter-spacing: 1px; padding: .4rem .6rem;
border: 1px solid #cdd9e3; background: #e8eef3; color: #1d3557; border-radius: 8px;
cursor: pointer; transition: all .1s; }
.guess:hover { background: #d7e2ec; }
.guess.sel { background: #1d3557; color: #fff; border-color: #1d3557; }
.readout { margin: .3rem 0 .8rem; }
.bar { height: 14px; background: #e6e8eb; border-radius: 7px; overflow: hidden; }
.fill { height: 100%; width: 0; background: #2a9d8f; transition: width .35s ease; }
.stat { font-size: .95rem; font-weight: 600; margin-top: .45rem; min-height: 1.3em; color: #1d3557; }
.pool { display: grid; grid-template-columns: repeat(5, 1fr); gap: 4px; margin: .3rem 0 .8rem; }
.w { font: 600 12px ui-monospace, monospace; text-align: center; padding: .35rem 0; border-radius: 6px;
background: #eef1f4; color: #555; transition: all .25s; }
.w.live { background: #d8f3ec; color: #0a6b5c; }
.w.dead { opacity: .28; text-decoration: line-through; }
.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; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Wordle as an INFORMATION problem. The secret is one of WORDS. Each guess, scored
// against a hidden secret, returns a colored pattern (green/yellow/gray). A guess
// partitions the candidate list into one bucket per possible pattern. The expected
// information gained is the ENTROPY of that bucket distribution, in bits:
// H = -Σ p(pattern) · log2 p(pattern).
// A high-entropy guess splits the words evenly, so on average it leaves far fewer
// candidates. This is exactly how optimal Wordle bots choose their openers.
const WORDS = ["CRANE","SLATE","TRACE","PLANT","BRICK","GHOST","JUMPY","VIVID","FUZZY","NYMPH","CRANK","SHINE","ROAST","DROWN","FLUTE","GRIME","BLOND","QUART","WALTZ","CHOMP"];
const GUESSES = ["CRANE","SLATE","ROAST","NYMPH","FUZZY","VIVID"];
// Standard Wordle feedback (two-pass, handles repeated letters): 2=green,1=yellow,0=gray.
function pattern(guess, secret){
const res=[0,0,0,0,0], sc=secret.split("");
for(let i=0;i<5;i++){ if(guess[i]===secret[i]){ res[i]=2; sc[i]=null; } }
for(let i=0;i<5;i++){ if(res[i]===0){ const j=sc.indexOf(guess[i]); if(j>=0){ res[i]=1; sc[j]=null; } } }
return res.join("");
}
// Expected information (bits) of a guess over the candidate set.
function entropy(guess, cands){
const b={}; for(const s of cands){ const p=pattern(guess,s); b[p]=(b[p]||0)+1; }
let H=0; const n=cands.length;
for(const k in b){ const pr=b[k]/n; H -= pr*Math.log2(pr); } return H;
}
// Average number of candidates left after this guess (over all possible secrets).
function avgRemaining(guess, cands){
const b={}; for(const s of cands){ const p=pattern(guess,s); b[p]=(b[p]||0)+1; }
let s=0; for(const k in b){ s += b[k]*b[k]; } return s/cands.length;
}
let selected=null;
const $=id=>document.getElementById(id);
function drawGuesses(){
const g=$("guesses"); g.innerHTML="";
for(const w of GUESSES){
const d=document.createElement("div");
d.className="guess"+(w===selected?" sel":""); d.textContent=w;
d.addEventListener("click",()=>select(w)); g.appendChild(d);
}
}
function drawPool(live){
const p=$("pool"); p.innerHTML="";
for(const w of WORDS){
const d=document.createElement("div");
d.className="w"+(live?(live.has(w)?" live":" dead"):"");
d.textContent=w; p.appendChild(d);
}
}
function select(w){
selected=w; drawGuesses();
const H=entropy(w,WORDS), avg=avgRemaining(w,WORDS), maxH=Math.log2(WORDS.length);
$("fill").style.width=(100*H/maxH).toFixed(1)+"%";
// Show one concrete split: pretend the secret is the first word and keep its bucket.
const fb=pattern(w,WORDS[0]);
const live=new Set(WORDS.filter(x=>pattern(w,x)===fb));
drawPool(live);
$("stat").textContent=fmt('“%w%”” carries %H% bits — on average it leaves %avg% of %length% words. (Example split shown: %size% left.)', { w: w, H: H.toFixed(2), avg: avg.toFixed(1), length: WORDS.length, size: live.size });
}
$("best").addEventListener("click",()=>{
let b=GUESSES[0]; for(const w of GUESSES) if(entropy(w,WORDS)>entropy(b,WORDS)) b=w; select(b);
});
$("worst").addEventListener("click",()=>{
let b=GUESSES[0]; for(const w of GUESSES) if(entropy(w,WORDS)<entropy(b,WORDS)) b=w; select(b);
});
drawGuesses(); drawPool(null); $("count").textContent=WORDS.length;
console.log("Entropy of each guess (bits):");
for(const w of GUESSES) console.log(w, entropy(w,WORDS).toFixed(3));
Comments
Loading comments...