Skip to content

Repository files navigation

2D Multiplayer Duel Game

A real-time 2D multiplayer duel game built with Next.js (App Router), TypeScript, Tailwind CSS, MongoDB (Mongoose), Server-Sent Events (SSE) + client-side interpolation. Two players (AI agents or humans) join, duel with bullets and jumps, and the winner is declared when an opponent loses all lives.

Features

  • Multiplayer Support: Exactly 2 players can join each game
  • Real-time Gameplay: Game updates in real-time with smooth animations
  • Simple Controls: Players can shoot and jump
  • Lives System: Each player starts with 3 lives
  • Auto-start: Game automatically starts after 3-second countdown when both players join
  • API Endpoints: Simple REST API for controlling players programmatically
  • Responsive Design: Works on both desktop and mobile devices

Game Mechanics

  • Shooting: Players can shoot bullets at each other with a cooldown period
  • Jumping: Players can jump to avoid incoming bullets
  • Collision Detection: Bullets hitting players reduce their lives
  • Win Condition: Game ends when one player loses all 3 lives

API Endpoints

All endpoints support CORS and can be accessed after deployment:

Join Game

POST /api/game/join
Content-Type: application/json

{
  "playerName": "YourName"
}

Response:
{
  "success": true,
  "playerId": "unique-player-id"
}

Shoot

POST /api/game/shoot
Content-Type: application/json

{
  "playerId": "your-player-id"
}

Jump

POST /api/game/jump
Content-Type: application/json

{
  "playerId": "your-player-id"
}

Get Game State

GET /api/game/state

Response:
{
  "success": true,
  "gameState": {
    "players": [...],
    "bullets": [...],
    "gameStatus": "waiting|countdown|playing|finished",
    "winner": "winner-name"
  }
}

Reset Game

POST /api/game/reset

Local Development

  1. Install Dependencies:

    npm install
  2. Run Development Server:

    npm run dev
  3. Open Browser: Navigate to http://localhost:3000

Deployment to Vercel

Option 1: Deploy via Vercel CLI

  1. Install Vercel CLI:

    npm install -g vercel
  2. Login to Vercel:

    vercel login
  3. Deploy:

    vercel
  4. Follow the prompts:

    • Set up and deploy? Yes
    • Which scope? (Select your account)
    • Link to existing project? No
    • What's your project's name? (e.g., duel-game)
    • In which directory is your code located? ./

Option 2: Deploy via Vercel Dashboard

  1. Push to GitHub:

    git init
    git add .
    git commit -m "Initial commit"
    git branch -M main
    git remote add origin https://github.com/yourusername/duel-game.git
    git push -u origin main
  2. Connect to Vercel:

    • Go to vercel.com
    • Click "New Project"
    • Import your GitHub repository
    • Click "Deploy"

Option 3: One-Click Deploy

Deploy with Vercel

Environment Configuration

You now NEED MongoDB (Atlas or local). Create a .env.local file:

MONGODB_URI="your-mongodb-connection-string"
MONGODB_DB=duel_game

If you want to experiment purely in-memory (NOT recommended for Vercel) you could revert to the legacy gameState.ts, but expect flickering on serverless cold starts as described below.

Project Structure (Current)

duel-game/
├── src/
│   ├── app/
│   │   ├── api/
│   │   │   └── game/
│   │   │       ├── join/route.ts
│   │   │       ├── shoot/route.ts
│   │   │       ├── jump/route.ts
│   │   │       ├── state/route.ts
│   │   │       ├── reset/route.ts
│   │   │       └── stream/route.ts          # SSE streaming endpoint (Node runtime)
│   │   ├── layout.tsx
│   │   └── page.tsx                         # Uses SSE + interpolation + fallback polling
│   ├── components/
│   │   ├── GameCanvas.tsx
│   │   └── GameControls.tsx
│   ├── lib/
│   │   ├── dbGameState.ts                  # Game logic w/ optimistic concurrency + simulation
│   │   ├── dbConnect.ts                    # Mongoose singleton connector
│   │   └── gameState.ts                    # (Legacy) in-memory implementation (unused in prod)
│   ├── models/
│   │   └── GameState.ts                    # Mongoose schema/model
│   └── types/
│       └── game.ts
├── public/
└── README.md

How to Play

  1. Join the Game: Enter your name and click "Join Game"
  2. Wait for Second Player: The game requires exactly 2 players
  3. Countdown: Once both players join, a 3-second countdown begins
  4. Play: Use the SHOOT and JUMP buttons to battle your opponent
  5. Win: Reduce your opponent's lives to 0 to win!

Technical Details

  • Framework: Next.js 15 (App Router)
  • Language: TypeScript
  • Styling: Tailwind CSS
  • Persistence: MongoDB (Mongoose) single-document authoritative state
  • Concurrency Control: Optimistic updates with version field (atomic updateOne w/ match on version)
  • Simulation: Server-side discrete time advancement upon each read/mutation; bullets use swept collision to prevent tunneling
  • Realtime Delivery: SSE @ ~4Hz + client interpolation (60 FPS animation) + adaptive fallback polling
  • Client Interpolation: Moves bullets, updates jump arc, smooths countdown locally between server ticks
  • Reset Logic: Monotonic version increment (never resets to 0) to avoid stale overwrites
  • API Stability: All endpoints idempotent or version-safe; joins guard against race conditions

Why Not WebSockets Yet?

SSE is simpler on Vercel (no custom server) and cheap for one-to-few spectators. WebSockets could further reduce JSON overhead but require additional infra or Vercel Edge functions constraints.

Edge vs Node Runtime

SSE route uses runtime = "nodejs" because Mongoose requires Node APIs not reliably available in Edge runtime.

Architecture Evolution & Lessons Learned

Phase Approach Issue Encountered Resolution
1 In-memory state in serverless routes Flickering / resets (each cold instance had its own copy) Introduced versioning concept (still fragile)
2 Added manual version guarding Still lost state on cold-start or parallel instance writes Migrated to MongoDB singleton document
3 Naive rapid polling (50ms) on clients Hit MongoDB Atlas read ops limits quickly Introduced adaptive polling + optimistic UI
4 Mongo read on every 50ms tick (multiple clients) Scaling concern & cost Switched to SSE stream + per-tick server fetch only
5 Occasional race on simultaneous joins Player name overwritten Added optimistic concurrency mutateState loop
6 Countdown sometimes froze after joins Stale overwrite by slow poll All state changes consolidated through mutateState
7 Deployment error using Edge SSE with Mongoose Edge runtime incompatibility Moved SSE to Node runtime

Key Patterns

  • Monotonic Version: Clients ignore any snapshot with a lower version to eliminate rollback flicker.
  • Optimistic Concurrency: updateOne w/ { _id, version } filter; retry loop caps at few attempts.
  • Client Interpolation: Allows reducing authoritative fetch rate 5–10× while preserving smoothness.
  • SSE Streaming: Replaces many client polls with a single push channel (auto-reconnect after 55s).

MongoDB Polling & Rate Optimization

Initial design polled every 50ms (~20 req/sec per viewer). Two viewers + AI agents easily exceeded free-tier read limits. Final design:

  • SSE push: 4 updates/sec (configurable) per connected client.
  • Fallback adaptive polling only if SSE fails: 0.8–5 req/sec depending on state phase.
  • Each reset / action causes a single targeted write; no redundant full-document rewrites beyond necessary fields.

If you need even fewer reads:

  1. Lower SSE frequency when gameStatus !== 'playing'.
  2. Send diffs instead of full snapshots (not yet implemented—straightforward future enhancement).
  3. Implement server-driven backoff when no active players.
  4. Cache last serialized payload in-memory and only re-query DB if enough ms elapsed or a mutation occurred.

Running Locally with MongoDB

Start a local MongoDB (e.g. brew services start mongodb-community) then run:

npm install
npm run dev

If MONGODB_URI is absent the connector defaults to mongodb://localhost:27017/duel_game.

Deploying Safely on Vercel

  1. Provide MONGODB_URI & MONGODB_DB in Vercel Project Environment Variables (Production + Preview).
  2. Confirm SSE route (/api/game/stream) uses Node runtime (DO NOT set edge).
  3. Ensure cold starts won't cause UI rollback: keep monotonic version increments (never reset to 0 in production resets).
  4. Monitor MongoDB metrics; if read ops creep up, dial down TICK_MS in the stream route or add idle throttling.
  5. Consider a periodic auto-reset if state remains finished or waiting for long durations to keep gameplay fresh.

Health Check

An operational health endpoint is available at:

GET /api/health

Response example:

{
  "status": "up",
  "db": "ok",
  "stateVersion": 42,
  "uptimeMs": 123456,
  "latencyMs": 18
}

Use this endpoint to warm the Mongo connection after deployment (first invocation triggers connect) and for uptime monitoring. A 503 status indicates the database could not be reached after retry attempts.

Extending / Forking

Potential improvements for advanced users:

  • WebSocket transport (binary diff frames)
  • Sharded per-room architecture (multiple concurrent matches)
  • Replay recording (append-only event log with deterministic re-sim)
  • Bot framework (pluggable AI strategies via serverless invocations)
  • Observability: expose metrics endpoint with counts (joins, shots, hits, latency)

Troubleshooting

Symptom Likely Cause Fix
Flickering or state rollback Receiving older version snapshot Ensure client ignores lower version (already implemented)
Countdown freezes Stale poll overwrote updated state All state must go through mutateState (already fixed)
Second player disappears Race on join Optimistic concurrency join logic (already fixed)
High Mongo read ops Excessive polling Rely on SSE + interpolation; adjust tick rate
Build fails on stream route Edge runtime with Mongoose Set runtime = 'nodejs' in stream route
Bullets pass through player Tunneling at high speed Swept AABB collision implemented

API Usage Examples

Using curl:

# Join game
curl -X POST https://your-app.vercel.app/api/game/join \
  -H "Content-Type: application/json" \
  -d '{"playerName": "Player1"}'

# Shoot
curl -X POST https://your-app.vercel.app/api/game/shoot \
  -H "Content-Type: application/json" \
  -d '{"playerId": "your-player-id"}'

# Jump
curl -X POST https://your-app.vercel.app/api/game/jump \
  -H "Content-Type: application/json" \
  -d '{"playerId": "your-player-id"}'

Using JavaScript:

// Join game
const response = await fetch("/api/game/join", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ playerName: "Player1" }),
});
const { playerId } = await response.json();

// Shoot
await fetch("/api/game/shoot", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ playerId }),
});

License

MIT License - feel free to use / adapt for learning, experiments, or as a starter template.

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Submit a pull request

Support

If you encounter any issues or have questions, please open an issue on GitHub.

About

A duel game that can be played by ai agents

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages