Introduction

Gomoku, or five-in-a-row, has the gentlest rules imaginable. Two players take turns dropping stones on the lines of a grid — one black, one white — and the first to line up five stones in a row, horizontally, vertically or diagonally, wins. Children learn it in minutes; you can play it on graph paper with a friend.

It feels perfectly fair. Both players want the same thing, the board is symmetric, and surely whoever plays better wins. But "perfectly fair" turns out to be wrong. On the standard 15x15 board with no opening restrictions, the player who moves first holds a decisive advantage — one strong enough to guarantee victory against any defense.

The reason is a single idea you can feel in your own hands: the double threat, a move that makes two winning lines at once. Your opponent can block only one of them.

Build the Double Threat

You are black and you move first. The engine plays white and defends as best it can. Your goal is not just five in a row — it is to reach a position with two threats at once, so that wherever white blocks, the other threat still completes your five.

<p class="hint">You are <b>black</b> and move first. White (the engine) blocks your strongest threat each turn. Aim for a <b>double threat</b>: one stone that makes <b>two</b> open fours at once — white can block only one, and your five is unstoppable.</p>
<div id="board" class="board"></div>
<div class="status" id="status">Your move. Click an empty point.</div>
<div class="btns">
  <button id="hint" type="button">Hint</button>
  <button id="reset" type="button" class="ghost">New game</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; }
.board { display: grid; grid-template-columns: repeat(9, 34px); gap: 0;
         background: #e7c993; border: 2px solid #b9924f; width: max-content;
         padding: 4px; border-radius: 6px; margin: .4rem 0; }
.pt { width: 34px; height: 34px; position: relative; cursor: pointer; }
.pt::before, .pt::after { content: ""; position: absolute; background: #9c7a3c; }
.pt::before { left: 50%; top: 0; width: 1px; height: 100%; transform: translateX(-.5px); }
.pt::after { top: 50%; left: 0; height: 1px; width: 100%; transform: translateY(-.5px); }
.stone { position: absolute; inset: 4px; border-radius: 50%; z-index: 2; }
.stone.b { background: radial-gradient(circle at 35% 30%, #555, #000); }
.stone.w { background: radial-gradient(circle at 35% 30%, #fff, #c2c2c2); border: 1px solid #999; }
.pt.win .stone { box-shadow: 0 0 0 3px #e63946; }
.pt.tip::after, .pt.tip::before { background: #137a3a; }
.pt.tip { outline: 2px dashed #137a3a; outline-offset: -3px; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.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; }
// Gomoku (free-style, five in a row) on a small 9x9 board. You play black and move first. Free-style Gomoku on 15x15 is SOLVED: the first player wins with perfect play (Allis, 1993). The engine of that proof is the DOUBLE THREAT — one move that creates two "open fours" at once. White can block only one, so black's five becomes unstoppable. Here white defends greedily (blocks black's most dangerous line) so you can feel how a fork beats any single reply.
const N = 9;
const EMPTY = 0, BLACK = 1, WHITE = 2;
const board = new Array(N * N).fill(EMPTY);
const DIRS = [[1, 0], [0, 1], [1, 1], [1, -1]]; // -, |, \, /
const idx = (r, c) => r * N + c;
const inB = (r, c) => r >= 0 && r < N && c >= 0 && c < N;
let over = false;

// Count stones of `p` in a maximal run through (r,c) along dir, plus whether the two ends are open. Used to recognise threats and wins.
function lineInfo(r, c, p, dr, dc) {
  let len = 1, openA = false, openB = false;
  let rr = r + dr, cc = c + dc;
  while (inB(rr, cc) && board[idx(rr, cc)] === p) { len++; rr += dr; cc += dc; }
  openA = inB(rr, cc) && board[idx(rr, cc)] === EMPTY;
  rr = r - dr; cc = c - dc;
  while (inB(rr, cc) && board[idx(rr, cc)] === p) { len++; rr -= dr; cc -= dc; }
  openB = inB(rr, cc) && board[idx(rr, cc)] === EMPTY;
  return { len, ends: (openA ? 1 : 0) + (openB ? 1 : 0) };
}

// Five (or more) in a row anywhere through the point just played.
function winningAt(r, c, p) {
  for (const [dr, dc] of DIRS) if (lineInfo(r, c, p, dr, dc).len >= 5) return true;
  return false;
}

// How many "open fours" would player p own after playing (r,c)? An open four is a four-in-a-row with an empty end — a one-move-from-five threat. Two of them from a single stone is the unstoppable double threat (a fork).
function openFoursAfter(r, c, p) {
  board[idx(r, c)] = p;
  let f = 0;
  for (const [dr, dc] of DIRS) {
    const info = lineInfo(r, c, p, dr, dc);
    if (info.len === 4 && info.ends >= 1) f++;
    if (info.len >= 5) f += 2; // already a win counts as overwhelming
  }
  board[idx(r, c)] = EMPTY;
  return f;
}

function emptyCells() {
  const out = [];
  for (let r = 0; r < N; r++) for (let c = 0; c < N; c++)
    if (board[idx(r, c)] === EMPTY) out.push([r, c]);
  return out;
}

// Find a black move that immediately wins or builds a double threat (fork).
function findFork(p) {
  let best = null;
  for (const [r, c] of emptyCells()) {
    if (openFoursAfter(r, c, p) >= 2) { best = [r, c]; break; }
  }
  return best;
}

// White's greedy defense: win if it can, else block black's most dangerous line.
function whiteMove() {
  for (const [r, c] of emptyCells()) if (winInfoAt(r, c, WHITE)) return [r, c];
  let best = null, bestScore = -1;
  for (const [r, c] of emptyCells()) {
    let s = 0;
    board[idx(r, c)] = BLACK; // would this point have been good for black?
    for (const [dr, dc] of DIRS) {
      const info = lineInfo(r, c, BLACK, dr, dc);
      if (info.len >= 5) s = Math.max(s, 1000);
      else if (info.len === 4 && info.ends >= 1) s = Math.max(s, 100 + info.ends);
      else if (info.len === 3 && info.ends === 2) s = Math.max(s, 30);
      else s = Math.max(s, info.len * 2 + info.ends);
    }
    board[idx(r, c)] = EMPTY;
    if (s > bestScore) { bestScore = s; best = [r, c]; }
  }
  return best;
}
function winInfoAt(r, c, p) {
  board[idx(r, c)] = p;
  const w = winningAt(r, c, p);
  board[idx(r, c)] = EMPTY;
  return w;
}

function setStatus(t, cls) {
  const s = document.getElementById('status');
  s.textContent = t; s.className = 'status ' + (cls || '');
}

function markWin(r, c, p) {
  for (const [dr, dc] of DIRS) {
    if (lineInfo(r, c, p, dr, dc).len >= 5) {
      // walk to one end, then mark the run
      let rr = r, cc = c;
      while (inB(rr - dr, cc - dc) && board[idx(rr - dr, cc - dc)] === p) { rr -= dr; cc -= dc; }
      while (inB(rr, cc) && board[idx(rr, cc)] === p) {
        const el = document.querySelector('.pt[data-i="' + idx(rr, cc) + '"]');
        if (el) el.classList.add('win');
        rr += dr; cc += dc;
      }
      return;
    }
  }
}

function render() {
  const b = document.getElementById('board'); b.innerHTML = '';
  for (let r = 0; r < N; r++) for (let c = 0; c < N; c++) {
    const d = document.createElement('div');
    d.className = 'pt'; d.dataset.i = idx(r, c);
    const v = board[idx(r, c)];
    if (v !== EMPTY) {
      const s = document.createElement('div');
      s.className = 'stone ' + (v === BLACK ? 'b' : 'w');
      d.appendChild(s);
    } else {
      d.addEventListener('click', () => play(r, c));
    }
    b.appendChild(d);
  }
}

function play(r, c) {
  if (over || board[idx(r, c)] !== EMPTY) return;
  const fork = openFoursAfter(r, c, BLACK) >= 2;
  board[idx(r, c)] = BLACK;
  render();
  if (winningAt(r, c, BLACK)) { markWin(r, c, BLACK); over = true; return setStatus('Five in a row — you win!', 'ok'); }
  if (fork) { setStatus('Double threat! Two open fours — white can block only one.', 'ok');
    console.log('Fork played at', r, c, '— white cannot stop both fours.'); }
  else setStatus('White is thinking...', '');
  setTimeout(() => {
    if (over) return;
    const wm = whiteMove();
    if (!wm) { over = true; return setStatus('Board full — draw.', ''); }
    board[idx(wm[0], wm[1])] = WHITE;
    render();
    if (winningAt(wm[0], wm[1], WHITE)) { markWin(wm[0], wm[1], WHITE); over = true; return setStatus('White made five. Try again — build a fork.', 'bad'); }
    if (fork) setStatus('White blocked one four — the other is still open. Complete your five!', 'ok');
    else setStatus('Your move. Click an empty point.', '');
  }, 250);
}

function hint() {
  if (over) return;
  document.querySelectorAll('.pt.tip').forEach(e => e.classList.remove('tip'));
  const f = findFork(BLACK);
  if (f) {
    const el = document.querySelector('.pt[data-i="' + idx(f[0], f[1]) + '"]');
    if (el) el.classList.add('tip');
    setStatus('Fork available: this point opens two fours at once.', 'ok');
  } else {
    setStatus('No fork yet — build two open threes that share a point first.', '');
  }
}

function reset() {
  board.fill(EMPTY); over = false; render();
  setStatus('Your move. Click an empty point.', '');
}

document.getElementById('hint').addEventListener('click', hint);
document.getElementById('reset').addEventListener('click', reset);
render();
Result
Console

Watch what happens. White can always answer a single threat — a row of four with an open end. But the moment you place a stone that opens two fours simultaneously, white has only one move and two fires to put out. That fork is the whole secret: the first player, given enough room, can always manufacture one. Press Hint to see the engine point at a forking square.

The Real Complexity

So who wins Gomoku, really? For the free-style game this is no longer a mystery.

  • It is solved. In 1993, Victor Allis (with H. J. van den Herik and M. P. H. Huntjens) proved that on the 15x15 board with no opening restrictions, the first player wins with perfect play. Gomoku is one of the few real games whose outcome is settled.
  • The proof was a search, not a formula. Allis introduced threat-space search and proof-number search — methods that chase forced sequences of threats — and used them to exhaustively confirm a winning line for the first player.
  • It is an m,n,k-game. Gomoku is the (15, 15, 5) member of a whole family: an m×n board where you need k in a row. The strategy-stealing argument already shows the second player can never have a winning strategy in such games; Allis showed the first player actually does.
  • Generalized, it is hard. Decide the winner of an arbitrary m,n,k position on a board that grows with the input and the problem becomes PSPACE-complete — the same wall that blocks games like chess and Go from any quick general solution.

That is why competitive Gomoku adds opening rules (Swap, Renju restrictions): without them, the first player's forced win makes the game one-sided. The double threat you built in the demo is exactly the engine of that proof.

Where It Matters

"Force a win through a chain of threats the opponent cannot answer" is a pattern that runs through far more than five-in-a-row:

  • Game-playing AI: threat-space and proof-number search, sharpened on Gomoku, became standard tools for solving and playing two-player games.
  • Automated reasoning: proving a position is won is structurally like proving a theorem — both search a tree for a forced, unanswerable line.
  • Adversarial planning: security, scheduling and pursuit problems often reduce to "can I create two threats my adversary can only half-cover?"
  • Teaching strategy: the fork is the clearest possible lesson in why moving first, and creating dilemmas, beats reacting move by move.

Understand the double threat in Gomoku and you have met the heart of adversarial search — the same engine behind chess programs and the broader question of P vs NP.

Conclusion

Gomoku hides a clean surprise: a game simple enough for a child is also solved. On the free 15x15 board the first player can force a win, and the lever is the double threat — a move that opens two winning lines so the defender, with a single reply, can never seal both.

So the next time five-in-a-row feels like a fair coin flip, remember what Allis proved in 1993: with perfect play it is not a coin flip at all. The first stone, played well, already decides the game — and the same threat-chasing search that settled Gomoku still powers the way machines reason about chess and beyond.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/gomoku/Content licensed under CC BY-NC 4.0.