Legal moves in Reversi: eight directions, one scan

A square is legal if at least one of eight directions can sandwich the opponent. Testing and flipping share a single scan, so there is no simulated move to roll back.

The rules of Reversi fit in one sentence: when you place a stone, every opposing stone sandwiched between the new one and one of yours flips. The tempting translation into code is place, scan, roll back. The version in Reversi does something else: one scan answers both questions.

Eight directions

Starting from the candidate square, walk each of the eight directions. A direction qualifies when it first meets one or more opposing stones and then one of your own:

const DIRECTIONS = [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]];

function flipsFor(board, row, col, who, size) {
  if (at(board, row, col, size) !== 0) return [];
  const flipped = [];
  for (const [dr, dc] of DIRECTIONS) {
    const line = [];
    let r = row + dr;
    let c = col + dc;
    while (inBounds(r, c, size) && at(board, r, c, size) === other(who)) {
      line.push(index(r, c, size));
      r += dr;
      c += dc;
    }
    if (line.length && at(board, r, c, size) === who) flipped.push(...line);
  }
  return flipped;
}

The line.length test is mandatory. When the neighbouring square is already yours, nothing was sandwiched, so the square is not legal.

Why not place and roll back

Simulation means copying the board, placing, scanning, discarding the copy, and leaving dirty state behind the moment you get it slightly wrong. One traversal here yields both whether the move is legal and which stones flip, so the move function only has to recolour those indices:

const flipped = flipsFor(board, row, col, who, size);
if (!flipped.length) return null;
const next = board.slice();
next[index(row, col, size)] = who;
for (const i of flipped) next[i] = who;

Complexity is not the problem, mobility is

Eight directions times at most six steps is a few dozen comparisons per empty square, which is fine to brute force on an 8x8 board. The easy AI difficulty just picks a random legal move. What actually decides strength is the evaluation:

Factor Weight
Corners, which can never flip back 120
Squares next to a corner, which hand it over negative
Mobility, the number of legal moves critical in the midgame
Stones flipped matters only in the endgame

One interaction detail that is easy to miss

The game ends when neither side has a legal move. When one side has none and the other does, that side passes rather than the game ending. Passing is flow, not rules, so it lives in nextTurn, which returns whose turn is next and whether a pass happened. The UI only renders the pass notice and never judges the rules itself.

Less rule code is better. If one scan can answer both is this legal and what flips, do not write two.

← Back to all posts

Comments

…