A fully-featured maze game: procedural mazes, difficulty levels, A* hints, accounts, per-user stats & progress, achievements, global leaderboards, and multiplayer — with a MongoDB-backed, Vercel-deployable API documented in Swagger.
- Features
- Demo
- Quick Start
- Installation
- Usage
- Architecture
- API Documentation
- Development
- Testing
- Deployment
- Contributing
- License
- Multiple Difficulty Levels: Easy, Medium, Hard, and Expert modes
- Procedural Maze Generation: Unique maze every time using Depth-First Search algorithm
- Start Gate: The maze stays frozen behind a see-through Play overlay; the timer only starts when you press Play (which also reshuffles the maze, so peeking ahead doesn't help)
- Timer & Scoring System: Compete for the best time and highest score
- Hint System: Get pathfinding hints when stuck (costs points)
- Pause/Resume: Pause the game anytime without losing progress
- Move Counter: Track your efficiency
- Persistent Statistics: Lifetime stats saved locally (and to your account when signed in)
- Achievements System: 8 achievements to unlock
- Sound Effects: Optional audio feedback
- Themes: 10 visual themes (Daylight, Midnight, Neon, Forest, Sunset, Ocean, Dracula, Mono, Candy, Volcano)
- Touch Controls: Swipe directly on the maze to move (plus an on-screen D-pad)
- Fully Responsive: Presentable and playable from desktop down to small phones (~360px) — swipe on the maze + on-screen D-pad, stacked layout, scrollable tables
- Socket.IO infrastructure: Room join/leave, position broadcast, and
game-finished events on the long-lived server (
npm start) - Note: the realtime backend is in place; a multiplayer frontend is on the roadmap. (Socket.IO does not run on the Vercel serverless deployment.)
- Accounts: Register / log in with JWT auth (bcrypt-hashed passwords)
- Password reset: Simple 2-step flow (verify username + email → set new password)
- Password UX: Show/hide toggles and confirm-password fields everywhere
- Editable profile: Change username, email, and password in-app
- Per-user stats: Games played/won, win rate, best score, streaks
- Progress tracking: Level & XP, per-difficulty breakdown, recent games
- Profile dashboard: All of the above in an in-app modal
- Guest play: Pick a leaderboard name on first launch — no account needed; sign in any time to save progress
- MongoDB-backed (database
maze-game), with a pluggable datastore layer: swap to PostgreSQL (DB_DRIVER=postgres) or in-memory at any time - Swagger / OpenAPI: interactive docs at
/api-docs, spec at/openapi.json - Global Leaderboards: Compete worldwide with timeframe filters
- RESTful API: Leaderboard, games, achievements, auth, users
- Game Sessions: Server-side game tracking
- Real-time Multiplayer: Socket.IO rooms (on the long-lived server)
- Rate Limiting + Winston logging + optional Sentry
- Serverless-ready: Deploys to Vercel as one Express function
- CI/CD Pipeline: Automated testing (Jenkins / GitHub Actions)
- Docker Support: Containerized deployment with Docker Compose
- Security: Helmet.js, rate limiting, open CORS for the public API
- Installable PWA: Offline-capable app shell (the game launches and deep
links work with no network), an in-app Install button (
beforeinstallprompt), app shortcuts + maskable icons in the manifest, and a service worker that auto-updates and precaches fresh assets on each version bump — all of it works on the static host (Render / GitHub Pages), no server required - Code quality: Project-wide Prettier, ESLint, Jest (53 tests)
- Accessibility & Responsive: ARIA labels, keyboard nav, focus styles; mobile-first layout that stacks the maze + controls above stats, keeps the account control on phones, and avoids horizontal overflow (verified 360–390px)
- SEO Optimized: Meta tags, sitemap, robots.txt
👉 Play Now
| Feature | Description |
|---|---|
| 🎯 Multiple Difficulties | Easy (11×15) to Expert (25×35) mazes |
| ⏱️ Timer | Real-time timer and best-time tracking |
| 💡 Hints | A* pathfinding hints (costs points) |
| 🏆 Achievements | 8 unique achievements to unlock |
| 📊 Leaderboards | Global rankings with timeframe filtering |
| 👤 Accounts & Stats | Sign in to track level, win rate, streaks |
| 🎨 Themes | 10 themes (Dracula, Sunset, Ocean, Neon, …) |
| 📱 Touch | Swipe on the maze (or D-pad) on mobile |
| 🔊 Sound | Optional sound effects |
| ⏸️ Pause | Pause and resume anytime |
# Clone the repository
git clone https://github.com/hoangsonww/The-Maze-Game.git
cd The-Maze-Game
# Start with Docker Compose (runs the API server)
docker-compose up -d
# API + Swagger docs at http://localhost:3000/api-docs
# (the game frontend is static — open index.html, or use the live Render site)# Clone the repository
git clone https://github.com/hoangsonww/The-Maze-Game.git
cd The-Maze-Game
# Install dependencies
npm install
# Start the API server (Express + Swagger + Socket.IO) on :3000
npm run dev # API + docs at http://localhost:3000/api-docs
# In another shell, open the static game frontend
npm run serve # opens index.html via http-serverThe frontend talks to https://maze-game-api.vercel.app by default. To point it
at a local backend, set window.MAZE_API_BASE = 'http://localhost:3000' before
the scripts load (or in the console).
- Node.js >= 18.x
- npm >= 9.x
- MongoDB (a free Atlas cluster works) — the active datastore for the API
- Docker (optional, for containerized deployment)
- PostgreSQL >= 13.x (optional — only if you switch
DB_DRIVER=postgres)
Tip
The API also runs with no database (in-memory driver) for quick local play and tests.
-
Clone the repository
git clone https://github.com/hoangsonww/The-Maze-Game.git cd The-Maze-Game -
Install dependencies
npm install
-
Configure environment
cp .env.example .env # Edit .env with your configuration -
Build for production
npm run build
-
Start the server
npm start
- Arrow Keys or WASD - Move your character
- Swipe on the maze - Move on touch devices (swipe directly over the maze board)
- P - Pause/Resume game
- H - Use hint (costs points)
- R - Generate a new maze
- On-screen D-pad - Alternative touch controls
Note
On phones the maze and controls sit at the top with stats below, and you can play entirely by swiping on the maze or using the on-screen D-pad.
Guide the coral player (top-left) to the lime exit (bottom-right) as quickly as possible with minimal moves.
Base Score: 100 points
Penalties:
- Time: 0.1 points per second
- Moves: 0.5 points per move
- Hints: 10-20 points per hint (difficulty-based)
Difficulty Multipliers:
- Easy: 1.0x
- Medium: 1.5x
- Hard: 2.0x
- Expert: 3.0x
See API Documentation for complete API reference.
Example: Submit Score
const response = await fetch('/api/v1/leaderboard', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
playerName: 'Player1',
score: 150,
completionTime: 45000,
difficulty: 'medium',
moves: 50,
}),
});A static vanilla-JS client (Render / GitHub Pages) talks to a serverless Express
API (Vercel) that fans out to one of three interchangeable datastores. A
standalone Python library (mazeforge) shares the maze concepts but ships
independently.
flowchart TB
subgraph client["Frontend — static (Render / GitHub Pages)"]
IDX["index.html + canvas"]
JS["game.js · ui-components.js<br/>auth.js · i18n.js"]
CSS["style.css — Wayfinder · 10 themes"]
SW["service-worker.js (PWA)"]
end
subgraph api["Backend — Express (Vercel serverless)"]
APP["app.js — middleware + routes"]
SWG["Swagger UI /api-docs · /openapi.json"]
REPO["repository.js · users.js"]
end
subgraph stores["Datastores (DB_DRIVER)"]
MONGO[("MongoDB · maze-game")]
PG[("PostgreSQL")]
MEM[["in-memory"]]
end
subgraph lib["mazeforge (Python)"]
GEN["11 generators"]
SOL["7 solvers"]
REN["ASCII / PNG render · CLI · pygame"]
end
JS -->|"fetch /api/v1/* (CORS *)"| APP
JS -->|"JWT bearer"| APP
IDX --- JS --- CSS
SW -.offline cache.- IDX
APP --> SWG
APP --> REPO
REPO --> MONGO
REPO --> PG
REPO --> MEM
GEN --> SOL --> REN
sequenceDiagram
participant Player
participant Game as game.js
participant API as Maze Game API
participant DB as datastore
Player->>Game: press Play → solve maze
Game->>Game: stop timer · compute score
Game->>API: POST /api/v1/leaderboard { name, score, time, moves }
API->>DB: insert leaderboard row
DB-->>API: created entry
alt signed in
Game->>API: POST /api/v1/users/me/games (Bearer)
API->>DB: update stats · streaks · level
DB-->>API: decorated stats
API-->>Game: 201 stats → refresh dashboard
end
Game->>API: POST /api/v1/achievements/unlock (if newly earned)
API-->>Game: 201 achievement
Game-->>Player: win modal · updated leaderboard
The-Maze-Game/
├── index.html # Game page (static frontend — Render / GitHub Pages)
├── src/ # Frontend source
│ ├── css/style.css # "Wayfinder" UI styles
│ ├── js/
│ │ ├── game.js # Game engine (canvas, A*, scoring, sessions)
│ │ ├── ui-components.js # Modals, leaderboard, achievements
│ │ └── auth.js # Accounts, profile & stats dashboard
│ ├── html/about.html # About page
│ └── python/ # `mazeforge` — installable maze library (gen/solve/render) + pygame player
├── api/
│ └── index.js # Vercel serverless entry (exports the app)
├── server/ # Backend
│ ├── app.js # Express app factory (serverless-safe)
│ ├── index.js # Local/Docker entry (+ Socket.IO + listen)
│ ├── swagger.js # OpenAPI spec + CDN Swagger UI
│ ├── database/
│ │ ├── driver.js # DB_DRIVER resolver
│ │ ├── repository.js # Leaderboard / games / achievements (3 drivers)
│ │ ├── users.js # Accounts + stats (3 drivers)
│ │ ├── mongo.js # MongoDB connection (db `maze-game`)
│ │ ├── connection.js # PostgreSQL pool (lazy)
│ │ ├── schema.sql # Full relational schema (auth platform)
│ │ └── schema-game.sql # Active gameplay + users_app tables
│ ├── services/auth.js # bcrypt + JWT
│ ├── routes/ # auth, users, leaderboard, game, achievements, …
│ ├── middleware/ # auth (JWT), errorHandler
│ └── utils/ # logger, email, sentry
├── __tests__/ # Jest suites (server: node env, client: jsdom)
├── scripts/ # build-frontend, gen-secret, check-db, seed, smoke, openapi…
├── .github/workflows/ci.yml # CI: format → build server → build frontend → docker → summary
├── Makefile # one-stop targets (make help)
├── Dockerfile.backend # API image (node) ┐ pushed to GHCR by CI
├── Dockerfile.frontend # static image (nginx) ┘
├── docker/nginx.frontend.conf # nginx config for the frontend image
├── docker-compose.yml # local stack: backend + frontend + mongo
├── vercel.json · .vercelignore # serverless routing
├── webpack.config.js · jest.config.js · manifest.json · service-worker.js
└── README.md · API_DOCUMENTATION.md
Frontend (vanilla, static — Render / GitHub Pages)
- Vanilla JavaScript (ES6+), HTML5 Canvas
- CSS3 (Grid/Flexbox), Service Worker (PWA)
- Fonts: Bricolage Grotesque · Sora · JetBrains Mono
Backend
- Node.js + Express.js
- MongoDB (active) · PostgreSQL (switchable) · in-memory (fallback)
- JWT (
jsonwebtoken) + bcrypt for accounts - Socket.IO (multiplayer), Winston (logging), optional Sentry
- Swagger / OpenAPI 3 (served from a CDN)
- Helmet.js, express-rate-limit
Build & Tooling
- Webpack 5, Babel 7
- Jest + Supertest
- Prettier, ESLint
DevOps
- Render (static frontend, canonical) · GitHub Pages (frontend, alt) · Vercel (serverless API)
- Docker & Docker Compose, Nginx
- Jenkins / GitHub Actions (CI/CD)
Complete API documentation is available in API_DOCUMENTATION.md.
Interactive docs (Swagger UI) are served at /api-docs; the OpenAPI spec at
/openapi.json.
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/health |
GET | Health + active DB driver | |
/api/v1/auth/register |
POST | Create an account | |
/api/v1/auth/login |
POST | Log in (username or email) | |
/api/v1/auth/me |
GET | 🔒 | Current account + stats |
/api/v1/auth/reset/verify |
POST | Reset step 1 (verify) | |
/api/v1/auth/reset |
POST | Reset step 2 (new password) | |
/api/v1/users/me/stats |
GET | 🔒 | Your stats & progress |
/api/v1/users/me/games |
POST | 🔒 | Record a finished game |
/api/v1/users/me |
PATCH | 🔒 | Edit username / email |
/api/v1/users/me/password |
POST | 🔒 | Change password |
/api/v1/users/:id |
GET | Public profile | |
/api/v1/leaderboard |
GET | Get leaderboard | |
/api/v1/leaderboard |
POST | Submit score | |
/api/v1/leaderboard/rank/:name |
GET | Player rank | |
/api/v1/games/start |
POST | Start game session | |
/api/v1/games/:id/move |
PUT | Record a move | |
/api/v1/games/:id/complete |
PUT | Complete game + score | |
/api/v1/games/stats |
GET | Aggregate game stats | |
/api/v1/achievements |
GET | List achievements | |
/api/v1/achievements/user/:id |
GET | A player's achievements | |
/api/v1/achievements/unlock |
POST | Unlock an achievement |
A Makefile wraps the common tasks — run make help for the full list:
make install-all # Node + Python deps
make dev # API with hot reload
make test-all # JS + Python tests
make build # static frontend -> frontend-dist/
make docker-build # build both Docker images
make ci # run the core CI gate locally
make secret # print a JWT secretscripts/ holds standalone utilities (build-frontend, gen-secret,
check-db, seed-leaderboard, smoke-test, export-openapi,
validate-openapi, clean-leaderboard) — see scripts/README.md.
.github/workflows/ci.yml runs in order: 1) format check → 2) build &
test the server (uploads server-dist) → 3) build the frontend (uploads
frontend-dist) → 4) build & push both Docker images to GHCR → 5) a
summary job. Images are linked to this repo via the OCI source label.
# Development
npm run dev # Start dev server with hot reload
npm run serve:static # Serve static files
# Building
npm run build # Production build with webpack
npm run lint # Run ESLint
npm run lint:fix # Fix ESLint issues
npm run format # Format the whole project with Prettier
npm run format:check # Check formatting (CI gate)
# Testing
npm test # Run all tests (53) with coverage
npm run test:watch # Watch mode
# Backend
npm start # Start the API server (server/index.js)
npm run db:migrate:pg # Apply the Postgres gameplay schema (DB_DRIVER=postgres)
npm run clean:leaderboard -- "E2E_*" # Remove leaderboard rows by name/prefix (needs MONGODB_URI)
# Python (the `mazeforge` library — see src/python/)
npm run py:play # install + launch the pygame player
npm run py:test # install + run the Python test suite (pytest)Create a .env file based on .env.example:
NODE_ENV=development
PORT=3000
HOST=localhost
# Datastore: mongo (default when MONGODB_URI set) | postgres | memory
DB_DRIVER=mongo
MONGODB_URI=mongodb+srv://user:pass@cluster.mongodb.net/
MONGODB_DB=maze-game
# Accounts (JWT)
JWT_SECRET=generate_a_long_random_string
JWT_EXPIRATION=7d
# Optional PostgreSQL (only when DB_DRIVER=postgres)
DATABASE_URL=Generate a strong JWT_SECRET:
node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"With nothing set, the API runs on the in-memory driver — fine for local play and tests.
- Game Features: Edit
src/js/game.js - UI Components: Edit
src/js/ui-components.js - API Endpoints: Add routes in
server/routes/ - Styles: Edit
src/css/style.css
src/python/ is a standalone, installable, typed Python package — the
algorithmic engine, usable by any app or dev independently of the web game.
- 11 generators: recursive backtracker, Prim, Kruskal, Wilson, Aldous-Broder, hunt-and-kill, Eller, recursive division, binary tree, sidewinder, growing tree.
- 7 solvers: BFS, DFS, Dijkstra, A* (Manhattan/Euclidean/Chebyshev), greedy best-first, wall follower, dead-end filling.
- Distance fields + longest-path (diameter), ASCII & PNG rendering, wall-grid
interop, seedable/reproducible, zero core dependencies,
py.typed.
pip install ./src/python # or pip install "./src/python[image,play]"import mazeforge as mf
maze = mf.generate(20, 30, "recursive_backtracker", seed=42)
sol = mf.solve(maze, algorithm="astar")
print(mf.to_ascii(maze, path=sol.path))mazeforge generate -r 20 -c 30 --algo wilson --solve # CLI
mazeforge play # pygame playerDetails: src/python/README.md. Tests: pytest src/python.
npm test# Backend tests
npm test -- __tests__/server
# Frontend tests
npm test -- __tests__/client
# Coverage report
npm test -- --coverage
# Python library tests (51 tests, pytest)
pip install "./src/python[dev]" && pytest src/python- JS unit tests: Game logic, scoring, pathfinding (Jest, 53)
- JS integration tests: API endpoints (Supertest)
- Python tests: every generator/solver/renderer (pytest, 51) + mypy-clean
- Overall: > 80%
- Statements: > 80%
- Branches: > 75%
- Functions: > 80%
- Lines: > 80%
flowchart LR
user(["Player"]) -->|HTTPS| FE["Static frontend<br/>Render / GitHub Pages"]
FE -->|"fetch /api/v1/*"| BE["Express API<br/>Vercel serverless"]
BE --> MDB[("MongoDB Atlas<br/>maze-game")]
dev(["Maintainer"]) -->|git push| GH["GitHub"]
GH --> CI["GitHub Actions CI"]
CI -->|"build + push"| GHCR["GHCR images<br/>backend · frontend"]
GH -->|deploy hook| FE
GH -->|deploy hook| BE
GHCR -.self-host.-> any["Any Docker host"]
.github/workflows/ci.yml runs five ordered jobs — a failure stops the chain:
flowchart LR
A["1 · format check<br/>prettier --check"] --> B["2 · build + test server<br/>↑ server-dist"]
B --> C["3 · build frontend<br/>↑ frontend-dist"]
C --> D["4 · docker build<br/>login GHCR · push backend + frontend"]
D --> E["5 · summary<br/>::notice pipeline success"]
Two images are built from the repo: Dockerfile.backend (the Express API)
and Dockerfile.frontend (the static site on nginx). CI publishes both to
GHCR, linked to this repo:
ghcr.io/hoangsonww/the-maze-game-backend
ghcr.io/hoangsonww/the-maze-game-frontend
Local full stack (backend + frontend + MongoDB):
docker compose up --build # API :3000 (/api-docs) · frontend :8080 · mongo :27017
docker compose --profile postgres up # also start PostgreSQL
make docker-build # just build both images
make docker-push # build + push to a registry (REGISTRY/OWNER/TAG vars)Pull the published images:
docker pull ghcr.io/hoangsonww/the-maze-game-backend:latest
docker pull ghcr.io/hoangsonww/the-maze-game-frontend:latest# Build for production
npm run build
# Start server
NODE_ENV=production npm startThe Express backend is serverless-ready and deploys to Vercel as-is:
vercel --prod- All requests are routed to
api/index.js(seevercel.json), which exports the shared app fromserver/app.js. GET /redirects to/api-docs— interactive Swagger UI whose assets (CSS, JS, favicon) load entirely from a CDN. The raw spec is served at/openapi.json.- The active datastore is MongoDB (database
maze-game). PostgreSQL is wired and correct but inactive; switch withDB_DRIVER=postgresand applyserver/database/schema-game.sql. With no DB configured the API falls back to an in-memory store. - CORS allows all origins, so the static frontend (Render / GitHub Pages) or anything else can call it directly.
- Accounts (
/api/v1/auth/register,/login,/me) issue JWTs (bcrypt-hashed passwords); per-user stats and progress live at/api/v1/users/me/statsand/me/games. SetJWT_SECRETto enable them. - Real-time multiplayer (Socket.IO) runs only via
npm start(server/index.js), not on serverless.
Environment variables to set in Vercel:
| Variable | Required | Notes |
|---|---|---|
MONGODB_URI |
yes | MongoDB connection string |
JWT_SECRET |
yes (accounts) | Long random string; signs auth tokens |
MONGODB_DB |
no | Defaults to maze-game |
DB_DRIVER |
no | mongo (default when MONGODB_URI set), postgres, or memory |
NODE_ENV |
recommended | production |
JWT_EXPIRATION, BCRYPT_ROUNDS |
optional | Token TTL (default 7d) / hash cost (default 10) |
DATABASE_URL / DB_* |
only for Postgres | Used when DB_DRIVER=postgres |
SENTRY_DSN |
optional | Enables error tracking |
RATE_LIMIT_WINDOW_MS, RATE_LIMIT_MAX_REQUESTS |
optional | Rate-limit tuning |
The static game frontend is vanilla JS and is hosted on Render (the-maze-game.onrender.com) — GitHub Pages works equally well. Point its API calls at the Vercel URL when you deploy the backend.
The live frontend runs as a Render Static Site: build command
node scripts/build-frontend.js (publishing frontend-dist/) — or serve the repo
root directly, since the app needs no build step. Relative asset paths and the
service worker's scope: ./ mean it just works at the domain root.
Automatic deployment via GitHub Actions to the gh-pages branch. Because all
paths are relative and the service worker is scoped relatively, the PWA installs
and runs offline under the project sub-path too.
Heroku
heroku create maze-game-app
heroku addons:create heroku-postgresql:hobby-dev
heroku addons:create heroku-redis:hobby-dev
git push heroku mainAWS
- Build Docker image
- Push to ECR
- Deploy to ECS/Fargate
- Configure RDS (PostgreSQL) and ElastiCache (Redis)
DigitalOcean
doctl apps create --spec .do/app.yaml- ✅ Helmet.js security headers
- ✅ bcrypt password hashing + JWT auth
- ✅ Rate limiting (
/api/*) - ✅ Input validation (username/email/password, score fields)
- ✅ Parameterized SQL (PostgreSQL driver)
- ✅ Output escaping for user-rendered content (leaderboard, profile)
- ✅ Open CORS — intentional for a public, read-mostly game API
- ✅ HTTPS via the host (Render / GitHub Pages for the frontend, Vercel for the API)
The API host serves only JSON + the Swagger docs (no same-origin app), so the strict Content-Security-Policy is disabled there; the game frontend ships separately as a static site (Render, or GitHub Pages).
- Never commit
.envfiles - Rotate secrets regularly
- Use environment variables for sensitive data
- Keep dependencies updated:
npm audit fix - Enable HTTPS in production
We welcome contributions! Please follow these steps:
- Fork the repository
- Create a feature branch
git checkout -b feature/amazing-feature
- Commit your changes
git commit -m 'Add some amazing feature' - Push to the branch
git push origin feature/amazing-feature
- Open a Pull Request
- Follow the existing code style
- Write tests for new features
- Update documentation as needed
- Ensure all tests pass:
npm test - Run linter:
npm run lint
This project is licensed under the MIT License - see the LICENSE file for details.
Son Nguyen
- GitHub: @hoangsonww
- Project: The Maze Game
- Thanks to all contributors
- Inspired by classic maze games
- Built with modern web technologies
Made with ❤️ by Son Nguyen