Last active
June 11, 2026 10:13
-
-
Save mg901/a84a5543f8112900becfa60fb72e171c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | |
| }); | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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 = 7bits (6 playable rows + 1 buffer). The buffer row prevents false positives from wrapping between columns.Placing a piece
Track the next available bit index per column (
heights[]). To avoid recomputing1n << BigInt(i)on every move, all possible bit masks are precomputed once intoBIT_MASKS[].Drop =
bitboard |= BIT_MASKS[heights[col]], then increment the height.Win detection
Shift the bitboard by the direction's stride, AND with itself — this finds pairs. Repeat to find quads:
Four directions, four shifts:
1(vertical),7(horizontal),6and8(diagonals).Why it's fast
The entire board state is two integers. Win detection is 3 operations per direction regardless of board size.