Skip to content

Repository files navigation

Connect 4

Connect 4

The classic four-in-a-row, with a bitboard AI opponent and a self-playing welcome screen.

Live Demo

Connect 4 screenshot

React 19 Vite 6 TypeScript 5 Zustand 5 Canvas 2D Deploy


A from-scratch Connect 4 rendered entirely on an HTML5 canvas, with a bitboard-based negamax AI offering three difficulty profiles, a polished welcome modal with an attract self-play demo running behind it, and full keyboard navigation throughout.

Features

Opponent modes

Mode Behavior
Easy Heuristic only — always blocks an immediate loss, always takes an immediate win, otherwise weighted-random with a center bias. Plays like a casual human. Runs in-process.
Medium Depth-7 iterative-deepening negamax with α-β + transposition table + killer-move heuristic. ~800ms budget. Occasionally picks the 2nd-best move when the gap is genuinely small, so it doesn't feel mechanical. Runs in a Web Worker.
Hard Same engine, uncapped depth (up to MAX_MOVES) with a ~2.5s budget. Iteratively deepens within the budget; positions with few moves remaining are fully solved — the score is the true game-theoretic value. Heuristic understands Connect 4's parity / zugzwang principle. Runs in a Web Worker.

Attract self-play — Two easy-difficulty bots play each other behind the welcome modal as ambient motion, pausing on each result to let the win pulse play before resetting.

Win sequence — When the winning move lands, the four pieces light up one by one (staggered fade-to-white) before the end-of-game banner appears, so the result has time to register.

Keyboard navigation — Toggle hints with K. Digit keys 17 drop into columns; Esc opens the in-game menu; cycle segmented controls in modals; Enter activates focused controls.

Accessibility — Visually-hidden buttons mirror the canvas-painted ones for screen readers, segmented controls implement the WAI-ARIA radiogroup pattern with roving tabindex, prefers-reduced-motion respected throughout.

How It Works

The AI runs a classic two-player game search on a bitboard representation of the position. Medium and hard run inside a Web Worker so the UI keeps animating during multi-second searches; easy bypasses the worker (microseconds — round-trip would dominate).

Board ──► fromGameBoard() — 49-bit Pascal-Pons encoding
              │
              ▼
       chooseMoveAsync() — main thread, returns Promise<col>
              │
              ▼
       Web Worker (medium / hard only)
              │
              ▼
       Iterative deepening (depth 1, 2, … until budget or full solve)
       └── Each pass seeds the next pass's move ordering via the TT
              │
              ▼
       Negamax + α-β at each ply
       ├── Transposition table (canonical 98-bit key: position | mask << 49n)
       ├── Move ordering: TT-best → killer moves → center-out static
       ├── Immediate-win shortcut (mate-soon score)
       └── Recurse into legal children
              │
              ▼
       Leaf nodes: evaluatePosition() heuristic
       ├── 69 precomputed winning-line bitmasks
       ├── Threat density (3-in-a-row = 50, 2 = 10, 1 = 1)
       ├── Parity-aware threat overlay — favored row +30, off-parity +6
       │     (Connect 4's zugzwang rule: first player wants threats on odd
       │      rows from the bottom; second player wants even rows)
       └── Center-column bonus
              │
              ▼
       Difficulty wrapper
       ├── Easy   → no search; heuristic + weighted random; in-process
       ├── Medium → depth-7 cap, 800ms budget; tighter sandbagging
       └── Hard   → unbounded depth, 2.5s budget; fully solves late game

All move generation, win detection, and evaluation happen on BigInt bitboards (49 bits = 7 columns × (6 rows + 1 sentinel)), so winning-line checks are four shift-and-AND operations rather than a quadruple loop.

Worker cancellation. When the player resets mid-search, the only reliable way to interrupt a synchronous negamax loop in a worker is worker.terminate() — web workers are single-threaded, so a cancel postMessage can't fire while the search is running, and SharedArrayBuffer (which would let the main thread set an abort flag via Atomics) needs COOP/COEP HTTP headers that GitHub Pages can't set. The client terminates and respawns; the in-flight promise is orphaned and GC'd, and the existing AI generation counter discards the result if it ever did arrive. Worker creation is ~10ms on respawn — invisible against the AI's minimum-think-time floor.

The renderer is a pure-canvas paint loop driven by requestAnimationFrame. Layers composite in order:

background vignette → hole back-shadows → pieces → support feet
       → yellow board face with even-odd hole cutouts
       → hole rims → top-arch highlight → hover ghost
       → "CONNECT4" title → in-game MENU button → end banner

Pieces are drawn before the board face; the board's even-odd fill then punches holes through the yellow, revealing the pieces underneath. Drop animations use a custom easing (gravity-then-damped-bounce) tracked in a separate AnimState that lives outside React.

Quick Start

npm install
npm run dev        # dev server at http://localhost:3000/react-connect4/
npm run lint       # eslint over src/
npm run build      # type-check + production bundle in ./dist
npm run preview    # serve the production bundle locally

Vite's base is set to /react-connect4/ in vite.config.ts so assets resolve correctly on GitHub Pages. The same base path applies to the dev server URL — use /react-connect4/, not /.

Tech Stack

  • React 19 — mounts the canvas, owns the modal layer
  • Vite 6 — dev server, build, GitHub Pages base path
  • TypeScript 5 — strict mode, project references
  • Zustand 5 — game store + subscribe-driven AI scheduler
  • Canvas 2D — everything board-related is canvas-painted
  • BigInt bitboards — 49-bit Pascal-Pons encoding for the AI engine
  • Poppins + Inter — display and body type via Google Fonts

Project Structure

src/
├── App.tsx                      Mount canvas, run rAF loop, orchestrate AI + attract
├── main.tsx                     React 19 createRoot entry
├── store.ts                     Zustand game store (state + actions)
├── constants.ts                 Board dimensions + shared types
├── helpers/index.ts             checkGameBoard + isBoardFull
│
├── ai/
│   ├── bitboard.ts              49-bit Pascal-Pons encoding + primitives
│   ├── eval.ts                  Threat-density heuristic with parity overlay
│   ├── search.ts                Negamax + α-β + TT + iterative deepening
│   ├── engine.ts                Difficulty profiles (easy / medium / hard)
│   ├── worker.ts                Web Worker entry: runs negamax off-main-thread
│   ├── engineClient.ts          Main-thread async client; terminate-to-cancel
│   └── attractDemo.ts           Self-playing demo behind the welcome modal
│
├── canvas/
│   ├── layout.ts                All measurements + hit-boxes from canvas size
│   ├── animations.ts            Mutable AnimState + easing + win-sequence timing
│   ├── input.ts                 Mouse / touch / keyboard wiring
│   └── scene.ts                 Paint pipeline (background → pieces → board → banner)
│
└── components/
    ├── WelcomeModal.tsx         Pre-game setup with arrow-key segmented controls
    ├── ResetConfirmModal.tsx    Mid-game "return to setup?" confirmation
    ├── KeyboardHintsToggle.tsx  Viewport-fixed keyboard-hints toggle (desktop only)
    └── GithubAttribution.tsx    Bottom-right author + source link

Deployment

CI is wired up via GitHub Actions:

The deploy workflow uses the modern actions/deploy-pages flow (no gh-pages branch). One-time setup on the repo:

  1. Settings → Pages → Build and deployment → Source: select GitHub Actions.
  2. Push to master (or run the workflow manually from the Actions tab).

If you fork the repo, also update the base value in vite.config.ts to match your repo name.

License

MIT

About

Connect 4 — Classic four-in-a-row with a bitboard negamax AI and self-playing welcome screen, rendered entirely on HTML5 canvas. React · Vite · TypeScript. Play it live.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages