Skip to content

Repository files navigation

Minesweeper

Classic Minesweeper, rebuilt as an animation study: every state change on the board — reveal, flip, explode, win — is a motion sequence rather than an instant repaint.

A round of Minesweeper: cells flip open in a wave from each click, a flag is planted, and a final click hits a mine that detonates and reveals the whole board.

Play it

Tech stack

React 18 + TypeScript Component model and strict typing across game state
Vite 6 Dev server and build, with @/ path alias to src/
Tailwind CSS + shadcn/ui Utility styling; Button is the single shared primitive
Framer Motion 11 All board, cell and route animations
react-explode, react-confetti-explosion Mine detonation and win effects
React Router 7 (HashRouter) Navigation; hash routing needs no server rewrites, so it works as-is on GitHub Pages
Vitest Unit tests over the pure game rules in src/core

Typography is an LED dot-matrix face (public/assets/fonts), applied globally to give the UI a hardware-display feel.

Architecture

The codebase splits along a single line: game rules are pure, React only renders them.

src/
├── core/                 # framework-free
│   ├── utils/            # generate-empty-board, place-mines,
│   │                     # calculate-adjacents, reveal-cell, get-distance
│   ├── typings/          # CellData, Coordinate, Difficulty
│   └── constants/        # difficultyTypes, TRANSITION_SPEED
└── ui/
    ├── hooks/            # useGameMechanics, useUpdateEffect, useIsFirstRender
    ├── components/       # board, cell, status-bar, animatedRoute
    └── pages/            # home (difficulty select), game
  • core/utils are pure functions over CellData[][] — each takes a board and returns a new one. No React, no mutation, no side effects, which is what makes core.test.ts a plain data-in/data-out test file with no rendering involved.
  • useGameMechanics is the only place game state lives. It composes those utils into the game loop (handleReveal, handleFlag, reset) and exposes a flat, serialisable snapshot: board, gameOver, gameWon, isGameStarted, lastClickedCell.
  • Components are dumb. Board and Cell receive state and callbacks; they own animation, not rules.
  • Difficulty lives in the URL. Home encodes the preset as query params (/game?rows=16&cols=30&mines=99), so a game configuration is a link and no global store is needed. Because a URL can say anything, Game validates the params and falls back to the easy preset, and the mine count is clamped to maxMines(rows, cols) — the same bound placeMines uses, so the counter in the status bar can never disagree with the board.

Two mechanics worth calling out:

  • Deferred mine placement. The board starts empty; placeMines runs on the first click with a 3×3 safe zone around it, so the opening move can never lose and always opens a region.
  • Flood fill. reveal-cell expands zero-adjacency regions with an explicit stack rather than recursion — no call-stack limit on large boards.

Animations

lastClickedCell is the key idea: the click coordinate is kept in state, and every cell derives its own animation timing from its distance to it.

Cascading reveal. Board computes a Manhattan distance from the clicked cell (get-distance) and multiplies it by TRANSITION_SPEED to get a per-cell revealDelay. Cells on the same distance ring animate together, so the reveal travels outward as a wave from the click instead of appearing all at once.

Cell flip. Each unrevealed cell is a cover <div> inside AnimatePresence; on reveal it exits with rotateY: 360 + fade, reading as a tile flipping over. The number or emoji underneath enters with a scale: 3 → 1 pop.

Detonation. The clicked mine renders a react-explode burst on top of the grid, while a translucent black overlay covers the board and fades out on a 1.2 s delay — the cascade finishes before the board dims.

Victory. Two confetti bursts fire from either side of the grid.

Reset. Board uses Framer Motion's imperative useAnimate with useUpdateEffect (a useEffect that skips the first render) to run a two-step sequence — fade/scale out, then back in — so a reset re-introduces the board rather than blanking it.

Route transitions. App wraps Routes in AnimatePresence mode='wait', keyed on location.key. AnimatedRoute gives every page the same opacity + scale 1.1 → 1 entrance and reversed exit, so the outgoing screen fully leaves before the next arrives.

Features

  • Three presets — easy (10×10, 10 mines), medium (16×16, 40), hard (16×30, 99)
  • First click is always safe
  • Right-click flagging, with a live count of unflagged mines
  • Timer that starts on the first move and stops on win or loss

Getting started

Requires Node.js 18+.

npm install
npm run dev       # http://localhost:5173
npm test          # game-rule tests
npm run lint
npm run build     # type-check + production build into dist/

Deployment

Pushing to main runs .github/workflows/deploy.yml: lint, test, build, publish to GitHub Pages. The build reads GITHUB_PAGES=1 to set Vite's base to /minesweeper/; locally base stays / so npm run preview works unchanged. Enable it once under Settings → Pages → Source → GitHub Actions.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages