Zebradoodle
Web / Games | 2025
A 2022 Java CLI prototype, reborn in the browser.
A weekend project rebuilt from a Java command-line Wordle clone I wrote between September 2022 and January 2023 in undergrad. The original lived in legacy/Zebradoodle.java and loaded 26 per-letter dictionary files off disk. This rebuild brings the same engine into the browser as React + React Router and adds three siblings — Quordle, Sedecordle, and Nerdle — that share the original scoring core.
No backend, no design system, no Tailwind. The whole thing builds as a static bundle and deploys to GitHub Pages. State (stats, streaks, daily-puzzle resume) lives in localStorage so refreshes never lose progress and the daily puzzle picks up where it left off.
Live at mustakimfs.github.io/zebradoodle. The repo's commit history reflects the rebuild dates; the algorithmic lineage goes back to the original 2022 prototype.
One scoring core, four very different boards.
The interesting part isn't any single mode — it's how the same two-pass scoring engine drives all four. Wordle is one board. Quordle stacks four. Sedecordle stacks sixteen. Nerdle replaces the word with an 8-character arithmetic equation and the dictionary with a tokenizer + evaluator that enforces operator precedence and exact integer division before any guess can even be scored.
src/lib/scoring.js.YYYY-MM-DD — no server, no sync, no “today's word” endpoint. Practice mode rolls a fresh random puzzle on demand.=, standard operator precedence (* / before + -), exact integer division (no fractions), no leading zeros. The bank in src/data/nerdleAnswers.js is re-validated on module load so a corrupt answer can never reach a player.Wordle is the most cloneable design of the decade.
The interesting question wasn't “can I clone Wordle” — anyone can. It was “can the same engine drive a 1-board, a 4-board, a 16-board, and a math-equation variant without forking the scoring logic four times.” The README's table shows the answer: yes, with one shared scoring function and per-mode dictionaries.
“A 5-letter guess, 6 attempts, color-coded feedback. Designed for daily ritual play with one shared puzzle across the player base.”
“My guess had two E's but only one got marked yellow! Is that a bug?”
“Replace the word with an arithmetic equation. Same UI, totally different validation logic.”
“legacy/Zebradoodle.java: a CLI Wordle that read 26 per-letter dictionary files off disk and scored guesses with a two-pass compare() method.”
Four modes, one core, zero servers.
Port the core, multiply the boards, then change the alphabet.
Wordle in React — port the Java scorer.
Started by porting Zebradoodle.java's two-passcompare() to JS as src/lib/scoring.js. Built the Board / Tile / Keyboard components against it, hooked up localStorage for stats, and shipped a 1-board Wordle that behaves byte-identically to the original CLI on the same dictionary.
Multiply the board — Quordle and Sedecordle.
Two new modes for the price of zero new scoring code. Quordle renders four parallel boards and dispatches each guess to the same scorer against four answers. Sedecordle does the same at 16x with a 21-guess budget instead of 9. The scoring contract didn't change — the layout and the budget did.
Nerdle — change the alphabet entirely.
The math mode is the interesting one. Equations are 8 characters over 0-9 + - * / =. Built a small tokenizer that splits an equation into tokens, an evaluator that respects precedence and rejects fractional division, and a validator that enforces the 8-char / single-equals / no-leading-zero rules. Only after validation does the scorer get to run. The answer bank is re-validated on module load so a corrupt entry can never reach a player.
First JS port of the scorer was a 20-line single-pass that walked the guess once. It worked on 95% of inputs and silently broke on the rest — most visibly on guesses like EERIE against EATER where the second E got marked yellow when it shouldn't have. Rebuilt against the original Java two-pass with a small set of regression tests (BOOKS / EERIE / SASSY / BLOBS) and the bug disappeared. The lesson generalizes: when in doubt, port the original algorithm, then then simplify if you can prove it's equivalent.
One scoring contract, four dictionaries, sixteen boards.
The shape is small. Each game mode is a page under src/pages/ that owns a board count, a guess budget, and a dictionary. All of them call the same scoring + state-machine primitives in src/lib/. The two interesting paths diverge on input: word modes go straight to the scorer; Nerdle validates the equation first.
function compare(guess, answer) {
const colors = Array(guess.length).fill('absent')
const remaining = answer.split('')
// Pass 1 — mark correct, consume from remaining
for (let i = 0; i < guess.length; i++) {
if (guess[i] === answer[i]) {
colors[i] = 'correct'
remaining[i] = null
}
}
// Pass 2 — for each non-correct letter,
// try to match against a still-remaining answer letter
for (let i = 0; i < guess.length; i++) {
if (colors[i] === 'correct') continue
const j = remaining.indexOf(guess[i])
if (j !== -1) {
colors[i] = 'present'
remaining[j] = null // consume — repeated letter safety
}
}
return colors
}// Grammar
// equation := expr '=' expr (8 chars total)
// expr := term (('+'|'-') term)*
// term := factor (('*'|'/') factor)*
// factor := integer
//
// Constraints
// * exactly 8 chars
// * exactly one '='
// * no leading zero (except literal 0)
// * '/' must yield an integer
// * standard precedence (* / before + -)
function isValidNerdle(eq) {
if (eq.length !== 8) return false
const [lhs, rhs, ...rest] = eq.split('=')
if (rest.length !== 0 || lhs == null || rhs == null) return false
if (!noLeadingZeros(lhs) || !noLeadingZeros(rhs)) return false
const l = evalExpr(lhs); const r = evalExpr(rhs)
return Number.isInteger(l) && Number.isInteger(r) && l === r
}Four boards under one front door.
The product is four pages and one home screen. Live at mustakimfs.github.io/zebradoodle — no auth, no install, no permissions. Every mode has a Daily and a Practice button; stats persist per mode in localStorage.


