Skip to content

Instantly share code, notes, and snippets.

@mg901
Last active June 11, 2026 10:13
Show Gist options
  • Select an option

  • Save mg901/a84a5543f8112900becfa60fb72e171c to your computer and use it in GitHub Desktop.

Select an option

Save mg901/a84a5543f8112900becfa60fb72e171c to your computer and use it in GitHub Desktop.
import { useState } from "react";
const COLS = 7;
const ROWS = 6;
const STRIDE = ROWS + 1;
const BITBOARD_SIZE = STRIDE * COLS;
const VISUAL_BOARD_SIZE = ROWS * COLS;
const VERTICAL_SHIFT = 1n;
const DIAGONAL_RIGHT_SHIFT = BigInt(STRIDE + 1);
const HORIZONTAL_SHIFT = BigInt(STRIDE);
const DIAGONAL_LEFT_SHIFT = BigInt(STRIDE - 1);
const BIT_MASKS = (() => {
const masks = [];
for (let i = 0; i < BITBOARD_SIZE; i += 1) {
masks[i] = 1n << BigInt(i);
}
return masks;
})();
const P1 = "red";
const P2 = "yellow";
export default function App() {
const { currentPlayer, handlePlay, board, handleReset, winner } = useGame();
const isGameOver = !!winner;
return (
<div className="app">
<div className="container">
<Selector
player={currentPlayer}
isGameOver={isGameOver}
onClick={handlePlay}
/>
<Board board={board} />
<button className="btn" onClick={handleReset}>
Reset
</button>
{winner && (
<div className="disc" data-player={winner}>
WON
</div>
)}
</div>
</div>
);
}
function Selector({ player, isGameOver, onClick }) {
return (
<div className="selector" data-player={player} data-game-over={isGameOver}>
{Array.from({ length: COLS }, (_, index) => {
return (
<button
key={index}
className="btn disc"
onClick={() => onClick(index)}
/>
);
})}
</div>
);
}
function Board({ board }) {
return (
<div className="board">
{board.map((player, index) => {
return <div key={index} className="disc" data-player={player} />;
})}
</div>
);
}
function useGame() {
const [state, setState] = useState(createGame);
const board = bitboardsToBoard(state.bitboards);
return {
board,
currentPlayer: state.currentPlayer,
handlePlay,
handleReset,
winner: state.winner,
};
function handlePlay(colIndex) {
setState((prev) => makeMove(prev, colIndex));
}
function handleReset() {
setState(createGame());
}
}
function createGame() {
const initialHeights = new Uint8Array(COLS);
for (let i = 0; i < COLS; i += 1) {
initialHeights[i] = i * STRIDE;
}
return {
bitboards: {
[P1]: 0n,
[P2]: 0n,
},
heights: initialHeights,
currentPlayer: P1,
moves: 0,
winner: null,
isDraw: false,
};
}
function makeMove(prevState, colIndex) {
if (
colIndex < 0 ||
colIndex >= COLS ||
prevState.winner ||
prevState.moves >= VISUAL_BOARD_SIZE
) {
return prevState;
}
const currentHeightIndex = prevState.heights[colIndex];
if (currentHeightIndex % STRIDE === ROWS) {
return prevState;
}
const { currentPlayer } = prevState;
const nextBitboard =
prevState.bitboards[currentPlayer] | BIT_MASKS[currentHeightIndex];
const nextHeights = new Uint8Array(prevState.heights);
nextHeights[colIndex] += 1;
const nextPlayer = prevState.currentPlayer === P1 ? P2 : P1;
const nextMoves = prevState.moves + 1;
const winner = checkWinner(nextBitboard) ? currentPlayer : null;
const isDraw = winner === null && nextMoves >= VISUAL_BOARD_SIZE;
return {
bitboards: {
...prevState.bitboards,
[currentPlayer]: nextBitboard,
},
heights: nextHeights,
currentPlayer: nextPlayer,
moves: nextMoves,
winner,
isDraw,
};
}
function checkWinner(bitboard) {
return (
checkDirection(bitboard, HORIZONTAL_SHIFT) ||
checkDirection(bitboard, DIAGONAL_RIGHT_SHIFT) ||
checkDirection(bitboard, VERTICAL_SHIFT) ||
checkDirection(bitboard, DIAGONAL_LEFT_SHIFT)
);
}
function checkDirection(bitboard, shift) {
const m = bitboard & (bitboard >> shift);
return (m & (m >> (2n * shift))) !== 0n;
}
function bitboardsToBoard(bitboards) {
return Array.from({ length: VISUAL_BOARD_SIZE }, (_, index) => {
const uiRow = Math.floor(index / COLS);
const uiCol = index % COLS;
const bitboardsRow = ROWS - 1 - uiRow;
const bitIndex = uiCol * STRIDE + bitboardsRow;
const bitMask = BIT_MASKS[bitIndex];
if (bitboards[P1] & bitMask) {
return P1;
}
if (bitboards[P2] & bitMask) {
return P2;
}
return null;
});
}
@mg901

mg901 commented Jun 11, 2026 •

Copy link
Copy Markdown
Author

Bitboard Connect Four — Core Intuition

The key insight: represent each player's pieces as a single 64-bit integer, then detect four-in-a-row using bitwise shifts.

Memory layout

Each column occupies STRIDE = 7 bits (6 playable rows + 1 buffer). The buffer row prevents false positives from wrapping between columns.

col0   col1   col2 ...
[0–6] [7–13] [14–20]

Placing a piece

Track the next available bit index per column (heights[]). To avoid recomputing 1n << BigInt(i) on every move, all possible bit masks are precomputed once into BIT_MASKS[].

const BIT_MASKS = (() => {
  const masks = [];

  for (let i = 0; i < BITBOARD_SIZE; i += 1) {
    masks[i] = 1n << BigInt(i);
  }

  return masks;
})();

Drop = bitboard |= BIT_MASKS[heights[col]], then increment the height.

  const nextBitboard =
    prevState.bitboards[currentPlayer] | BIT_MASKS[currentHeightIndex];

Win detection

Shift the bitboard by the direction's stride, AND with itself — this finds pairs. Repeat to find quads:

function checkDirection(bitboard, shift) {
  const m = bitboard & (bitboard >> shift);

  return (m & (m >> (2n * shift))) !== 0n;
}

Four directions, four shifts: 1 (vertical), 7 (horizontal), 6 and 8 (diagonals).

Why it's fast

The entire board state is two integers. Win detection is 3 operations per direction regardless of board size.

Connect-Four-Game

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment