DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

Building Checkers from scratch: forced jumps, kings, and a minimax AI

🔴 Play it (you vs the AI): https://dev48v.infy.uk/game/day31-checkers.html

Checkers looks like a kids' game until you try to code it properly. The board is trivial. The rules that make it interesting — mandatory captures, multi-jump chains, kinging — are exactly the ones people skip, and they're the ones that make the AI worth writing. Here's how I built the whole thing in one file of vanilla JavaScript on a canvas: real rules, and a minimax opponent that actually looks ahead.

The board is half a board

Pieces in checkers only ever touch the dark squares, so an 8×8 grid is really 32 usable cells and every move is diagonal. I stored it as a plain 8×8 array of small integers: 0 empty, plus a separate code for each side's men and kings. Three rows of men at each end gives twelve pieces a side. The light squares just stay empty forever.

The one helper everything leans on is stepDirs(piece): a plain man moves one way only — my men up the board, the AI's down — while a king moves along all four diagonals. Return the allowed (dr, dc) steps for a piece and every other rule becomes a loop over those steps, with no special cases for colour or rank.

Capturing is a jump, and it's not optional

You don't capture by bumping into a piece — you jump it. If a touching enemy sits on one of your diagonals and the square immediately beyond is empty, you hop over, land two squares away, and that enemy comes off. A jump needs three squares in a line: your piece, the enemy, an empty landing.

The rule that gives checkers its teeth: if you can capture, you must. A quiet slide is illegal whenever any jump exists for your side. In code this is a single decision — generate every jump first; if that list isn't empty, return only jumps; otherwise fall back to slides:

function genMoves(b, pl) {
  const jumps = [];
  for (let r = 0; r < 8; r++) for (let c = 0; c < 8; c++)
    if (owner(b[r][c]) === pl)
      for (const j of jumpsFrom(b, r, c, b[r][c], []))
        jumps.push({ from: [r, c], to: j.to, captures: j.captures });
  if (jumps.length) return jumps;          // forced to capture
  // ...otherwise gather plain slides
}
Enter fullscreen mode Exit fullscreen mode

That single if matters more than it looks. Forced captures collapse the branching factor — on lots of turns there's only one legal piece to move, sometimes only one legal move — which is a gift to the search later.

Multi-jumps want recursion

A capture isn't one hop. If the piece that just landed can jump again, it must, and it keeps going until it can't. One turn can zig-zag two or three pieces off the board. The clean way to enumerate that is recursion: simulate the hop on a copied board, then call the same jump-finder from the new square, threading the list of already-captured squares so you never jump the same piece twice. Each complete chain becomes one move whose captures list holds everything it swept up.

Kinging flips a piece's whole personality

Reach the far rank — my men to row 0, the AI's to row 7 — and the man is crowned a king on the spot, free to move and capture both directions. That direction flip is the biggest upgrade in the game, which is why the evaluation values a king far above a man. One rule I got right on purpose: if a man is crowned in the middle of a jump chain, English draughts ends the turn — it doesn't keep jumping as a fresh king. So the recursion stops the moment a piece crowns.

The AI: minimax + alpha-beta

Winning is simple to detect: a side loses the instant it has no legal move — no pieces left, or every piece blocked. Same test either way, which gives the search a crisp terminal condition.

The opponent is textbook minimax. The AI plays each of its moves, imagines my best reply, and recurses down to a chosen depth. Leaf positions are scored by material — a man is 100, a king about 175 — plus a little geometry: hold the centre files, push men forward, keep the back row intact to block promotion. Alpha-beta pruning carries the best-so-far bounds for each side and throws away branches that can't change the decision, so the same depth costs far less. Because checkers keeps the branching factor small (and forced captures often shrink it to one), a plain minimax reaches a genuinely useful depth in real time — try the depth selector and feel the trade between strength and thinking time.

The demo has three tabs: play it, step through how each rule works, then build it yourself block by block. It's all offline, no libraries, no images — one array and a handful of pure functions.

👉 Play, then read the code: https://dev48v.infy.uk/game/day31-checkers.html

Top comments (0)