Brainteasers
Codebreaker
Guess a hidden code of colored pegs, one row at a time, using the clues each guess earns, before you run out of rows.
On this page
Codebreaker is the classic code-breaking game (Mastermind, from 1970), under a name David picked. A code of colored pegs hides behind a shield, and you guess it one row at a time. Each guess earns key pegs: red for a right color in the right hole, white for a right color in the wrong hole. Break the code before the rows run out.
It’s one of the site’s brainteasers (ADR 0042): it runs on the pencil-puzzle kit in the toy-box look and is scored on guesses first, then time. The pencil-puzzle kit covers posting, boards and the Stats tab; Brainteasers covers what’s different for a brainteaser, including no dailies or archive (ADR 0043). This chapter is what’s Codebreaker’s own.
At a glance#
| Part | Where | What it is |
|---|---|---|
| Engine | games/static/games/codebreaker/engine/index.js |
Levels, hiding a code, scoring a guess, judging an answer, the helpers’ arithmetic |
| Interface | games/static/games/codebreaker/js/ |
board.js (the peg board, the tray, filling and checking a row), puzzle.js (words, helpers, How to play, the share line), art.js (pegs, layout, emblem) |
| Look | games/static/games/codebreaker/codebreaker.css |
Teal accent; the slate board, the shield, the pegs’ colors |
| Server | puzzles/codebreaker.py |
Its Kind: levels, no dailies, the answer’s shape, judged and scored |
| Pages | games/templates/games/codebreaker.html, games/templates/games/thumbs/codebreaker.html |
The toy box’s page; the box art |
| Tests | tests/test_codebreaker.py |
The Kind, the answer’s shape, the real referee, the boards, the page |
| Design notes | docs/codebreaker.md |
The rules as played, the helpers, open questions |
Decisions: ADR 0042 (brainteasers), ADR 0043 (endless, no daily), ADR 0023 (the kit), ADR 0030 (ratings).
The rules in code#
A level is a size, as in Low Poly, so a player picks a level alone and a level names a board:
| Level | Colors | Holes | Rows |
|---|---|---|---|
| beginner | 6 | 4 | 10 |
| classic | 6 | 4 | 10 |
| super | 8 | 5 | 12 |
puzzles/codebreaker.py, LEVELSBeginner never repeats a color, in the code or a guess. Classic is the boxed game, and Super is Super Mastermind. Scoring a guess is the whole game. Exact matches count first. Then each color scores as many near matches as it has left over on both sides, so each code peg answers at most one guess peg:
games/static/games/codebreaker/engine/index.js score · lines 48–66/**
* The key pegs for a guess: { exact, near }. `exact` pegs are the right color
* in the right hole; `near`, the rest of the colors the code has as many of,
* in other holes (each code peg answers one guess peg at most).
*/
export function score(secret, guess) {
let exact = 0;
const left = new Map(), tried = new Map();
for (let i = 0; i < secret.length; i++) {
if (guess[i] === secret[i]) exact++;
else {
left.set(secret[i], (left.get(secret[i]) || 0) + 1);
tried.set(guess[i], (tried.get(guess[i]) || 0) + 1);
}
}
let near = 0;
for (const [color, n] of tried) near += Math.min(n, left.get(color) || 0);
return { exact, near };
}
A guess is final, as on the real board. The module sets undo: false, so the dock has no Undo, Redo or Restart, and Z and Y do nothing. The row you’re filling (the draft) can change until you Check it.
The game ends one of two ways. Broken: the last guess is the code, and answer() returns every guess. Out of rows: every row is used without breaking it (lost()). The kit ends that game without a solve. Nothing is posted, no streak grows, the shield lifts on the code, and the result sheet says “Out of rows”.
Making a code#
games/static/games/codebreaker/engine/index.js generate · lines 36–46/** The hidden code for a code and level: { engine, slug, code, size, difficulty, colors, holes, repeats, rows, secret }. */
export function generate(code, size, difficulty) {
const level = SIZES[size];
if (!level || difficulty !== level.difficulty) throw new Error(`Codebreaker doesn't come in ${size} ${difficulty}.`);
if (!CODE_RE.test(String(code))) throw new Error(`${code} isn't a code.`);
const r = stream(`${SLUG}|${ENGINE_VERSION}|${size}|${code}`);
let secret;
if (level.repeats) secret = Array.from({ length: level.holes }, () => r.int(level.colors));
else secret = r.shuffle(Array.from({ length: level.colors }, (_, i) => i)).slice(0, level.holes);
return { engine: ENGINE_VERSION, slug: SLUG, code: String(code), size, difficulty, colors: level.colors, holes: level.holes, repeats: level.repeats, rows: level.rows, secret };
}
The code comes from the kit’s seeded stream (the seeded stream) on codebreaker|<version>|<level>|<code>, so a puzzle code hides the same pegs in every browser and in the referee. Beginner shuffles the colors and takes the first four, so none repeats; other levels draw each hole independently. ENGINE_VERSION is 1; bump it if any code would hide different pegs.
There’s nothing to prove about a code, since any code can be broken (the first guess might be it). Difficulty is the level’s shape: Super has 32,768 possible codes against Classic’s 1,296. Brainteasers are endless (ADR 0043), so every code is fresh, at the level you pick.
Your move#
firstMove gives the kit’s first-move prompt (Your first move) its words: Make your first guess (“Tap pegs in the tray to fill your row, then Check”). It shrinks into the tray (tray: true), not the board, and the tray rings until the first peg.
Secrets#
The browser makes the puzzle, so it holds the code, and anyone with developer tools can read it. That’s the trade every solo game on the site makes (ADR 0003), and David’s call here too: the boards are family boards.
Posting and the referee#
An answer is every guess, in order, as color numbers. The server checks its shape first:
puzzles/codebreaker.py clean · lines 14–23def clean(answer, size):
"""An answer is every guess, in order: a list of rows, each a color
(0 up to the level's colors) for every hole. [[0, 3, 3, 1], [2, 3, 1, 5]]"""
colors, holes, rows = LEVELS[size]
if not isinstance(answer, list) or not 0 < len(answer) <= rows:
raise ValueError("not a list of guesses")
for guess in answer:
if not (isinstance(guess, list) and len(guess) == holes and all(type(c) is int and 0 <= c < colors for c in guess)):
raise ValueError("not a guess")
return answer
There’s no single right answer to compare with, so the referee replays the guesses under the engine’s own rules (Kind.judged, Referees and anti-cheat):
games/static/games/codebreaker/engine/index.js check · lines 110–120/**
* Whether an answer (every guess, in order) breaks the code by the rules:
* legal guesses, no more than the rows, the last one the code and none
* before it.
*/
export function check(puzzle, answer) {
if (!Array.isArray(answer) || answer.length < 1 || answer.length > puzzle.rows) return false;
if (!answer.every(g => legal(puzzle, g))) return false;
const hit = g => g.every((c, i) => c === puzzle.secret[i]);
return hit(answer[answer.length - 1]) && answer.slice(0, -1).every(g => !hit(g));
}
Every guess must be legal at its level, there can’t be more guesses than rows, and the code must come last and only last. The referee returns the guess count (the engine’s movesOf), the solve keeps it (Solve.moves), and the boards rank fewest guesses first, then time, then who posted first. A first guess can be the code, so the minimum time is only a few seconds.
Rating#
Like any endless kit puzzle (Ratings): a solve is rated on its time against par, at half weight. A lost code isn’t rated, because the kit keeps no losses (ADR 0042).
The interface#
The board is its own SVG, not a cell grid. Rows run from the bottom up, so your first guess is nearest you, as on the boxed board. The shield is along the top, and key pegs sit in a well beside each row. layout() in art.js places everything in units of one hole. Each row’s number sits in a margin 0.9 of a hole wide, at .42 of a hole: as large as “12” goes there with room either side, as the design language asks of words drawn on a board. “CODE” on the shield is .56. Both have their own ink on the slate, --board-ink (4.9:1 on the board) and --shield-ink (4.6:1 on the shield).
- Filling a row. A tapped tray peg goes in the picked hole (ringed in white), and the ring moves to the next empty one. Tap a hole to pick it, again to take its peg out. Backspace removes the picked hole’s peg, or the row’s last. In Beginner, a color already in the row moves to the hole you’re filling.
- Check commits the guess. The board applies it, hands it to the kit as one move, and the key pegs pop in. The button fills with the accent once every hole has a peg.
- Keys: 1–6 (1–8 in Super) pick a color, ← → move between holes, Backspace removes, Enter checks. The numbers are shortcut keys, so Options can turn them off.
- The count sits beside the clock (“3 guesses”), and leads the result sheet.
- Helpers. Shapes pressed into the pegs, so no color relies on color alone (on). A one-time warning when your clues so far rule out the guess you’re placing (
ruledOutBy, on). And how many codes still fit every clue (remaining, counted over every code at the level), off by default because it gives a lot away. - Ending. Broken, the board shines, the shield lifts, and the result sheet shows the code as its emblem. Out of rows or given up (the dock’s Give up, or the row’s in “Pick up where you left off”), the shield lifts with an ink sticker, “Out of rows” or “Gave up”. Share result scores it as the word games do, “4/10” (X/10 when the rows ran out), with a line of key pegs per guess (🔴 right hole, ⚪ right color, ⚫ neither), never the colors (
outOfandshareLineinpuzzle.js).
Its look#
Codebreaker’s personality, in the design system’s personality sheet. Everything else is the toy box’s (Brainteasers), on the pencil-puzzle kit.
| Family | Brainteasers: the pencil-puzzle kit, in the toy box’s look |
| Palette | Its accent, deep teal #0d6a70: the one fill (Check, once the row is full), the ribbon and the sticker. The board is slate plastic #2a3342, its holes #131820, and the shield over the code #3b4658. Words drawn on them have their own ink: #96a2b4 for the row numbers, #aab5c4 for CODE. Key pegs are red #e5383b for the right color in the right hole and white #f4f6f8 for a right color elsewhere, as the 1970s board had them. The code pegs are eight colors of their own (--peg-0…7). White rings the hole your next peg goes in. |
| Faces | The toy box’s: Rubik |
| Corners and edges | The toy box’s. The tray’s pegs are round buttons on a lip of the board’s edge that press down. |
| Board | The boxed game’s board in slate plastic, rows numbered faintly down its left: your first guess at the bottom, nearest you, the rest climbing toward a hinged shield lettered CODE over the code at the far end. Code pegs are glossy domes, each with a shape pressed into it (circle, triangle, square, diamond, plus, star, heart, hexagon), on by default, so no peg is told by color alone. Key pegs sit small in a well beside each row. Your row wears a lighter strip and a white ring on the next hole. The tray under the desk holds the pegs as round buttons, Take out, and Check. |
| Emblem and art | Three pegs and a key peg, as they stand in a row; a broken code’s result sheet shows the code in its place. Its box: the peg board, whole, from above on the table. |
| Voice | A code is its noun (“12 codes”) and guesses its score; an ended board’s ink sticker says “Out of rows” or “Gave up”. |
| Payoff | Key pegs pop in one after another. Broken, the shield lifts, the code’s pegs pop, and a sticker in the accent goes on the tray; the result sheet shows the code as its emblem. Out of rows or given up, the shield lifts with an ink sticker. |
| The glance | Now: the status line. The score: the guess count, beside the clock in the bar (“3 guesses”). |
| Exceptions | None |
Changing it#
- Bump
ENGINE_VERSIONif a code would hide different pegs. - A new level is a size in the engine’s
SIZES, plus theKind’ssizesandLEVELS. It needs no daily slot (dailies=False), only a place in theKind’sdailyorder. npm run check:puzzlesruns the engine through the kit’s checks (determinism, fixed codes, wrong answers refused).