DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

Mastermind needs no rules engine — just two arrays of four numbers and one duplicate-safe two-pass scoring trick

Mastermind looks like it should need a rules engine — a notion of pegs, colours, positions, and some grader that "knows" the game. It needs none of that. The hidden code is an array of four numbers. Your guess is an array of four numbers. The entire game turns on one small scoring function that grades a guess into two tallies. About 150 lines of vanilla JavaScript, no canvas, no library. Here's how I built it.

Colours are just indices

There's no "red" or "blue" anywhere in the logic. A colour is an index, 0 to 5. That one decision makes everything downstream arithmetic: the secret is four indices, a guess is four indices, and comparing them is comparing numbers. The palette is the only place a colour ever becomes a real hex value, and only for drawing. Resetting the game is the same as building a whole new board, because the game is those few variables — a secret array, a list of past guesses, and the row you're editing.

let secret  = [4,0,4,2];              // the hidden code
let current = [null,null,null,null];  // the row you're editing
let rows    = [];                     // past guesses + their scores
Enter fullscreen mode Exit fullscreen mode

The one clever function — a two-pass score

This is the only interesting code in the game, and the one everybody gets wrong on the first try. Pass one walks both rows together and counts the holes that match exactly in colour and position — those are black pegs — and for the holes that don't match, it banks the leftover colours on each side. Pass two matches the remainders: for each colour, the white pegs it earns is the smaller of what the guess has left and what the code has left.

function scorePegs(guess, code){
  let black = 0;
  const codeLeft = {}, guessLeft = {};
  for (let i = 0; i < code.length; i++){
    if (guess[i] === code[i]) black++;            // exact hit
    else {                                        // bank leftovers
      codeLeft [code[i]]  = (codeLeft [code[i]]  || 0) + 1;
      guessLeft[guess[i]] = (guessLeft[guess[i]] || 0) + 1;
    }
  }
  let white = 0;
  for (const c in guessLeft) white += Math.min(guessLeft[c], codeLeft[c] || 0);
  return { black, white };
}
Enter fullscreen mode Exit fullscreen mode

Why the duplicate case works

That Math.min is the whole trick. Because exact matches are removed first and each side is counted only by its remainder, no single peg is ever counted twice — one code peg can't answer two guess pegs of the same colour, and two code pegs can't be over-claimed by one. Skip the two passes and duplicate colours give nonsense scores; it's the bug every first attempt ships. Those two numbers, black and white, are the only information the game ever hands you, and the win condition is just black === 4.

The search space is only 1296

Deducing the code is possible because the space is small: four pegs, six colours, repeats allowed, is 6⁴ = 1296 codes. Every score you see partitions that set — only the codes that would produce the same black/white against your guess survive; the rest are logically impossible. A black-heavy score means you're close on positions; whites mean right colours you've misplaced; a zero on a colour rules it out of the code entirely. Good play is really information theory: pick guesses whose scores slice the survivors thin.

Flip sides — the AI cracks your code

The same score function lets the computer break your code. Start with all 1296, and after each guess keep only the codes consistent with the feedback. To pick the next guess, Knuth's 1977 rule is minimax: for every candidate, look at how the survivors split across all possible scores, and play the one whose largest resulting group is smallest — the guess that guarantees the biggest cut no matter what comes back. Starting from the proven opener [0,0,1,1], it cracks any 4×6 code in five guesses or fewer. The solver and the human player share the exact same scorePegs — one for grading you, the reverse to grade the computer's guesses against the code you set.

The board is pure derived HTML

There's no board object to keep in sync. For each of the ten rows I render either a submitted guess with its black/white pegs, the one active editable row, or an empty row waiting — the feedback is a little 2×2 of dots, black first, then white, then empty. Because the state is so small, I rebuild the whole board from it on every change; re-rendering costs nothing and there's no stale UI to chase.

Set a code, read the pegs, and deduce — or flip to AI-breaks and watch minimax do it:

https://dev48v.infy.uk/game/day52-mastermind.html

Top comments (0)