Retro roguelike browser-based Plinko built with React, Vite, and Matter.js.
- Physics-driven peg board using Matter.js.
- Coin economy with upgrades and scaling costs.
- Slot progression and payout growth over time.
- Toggleable settings panel with sound on/off and volume slider.
- Main nav tabs for
PlayandLeaderboardviews. - New
Clanstab with create/join flows, clan leaderboard, and clan home. - Real-time clan chat (Socket.IO) scoped to clan membership.
- Clan war timeline with bi-weekly cycle and 3-day active war window.
- Persistent Node.js global leaderboard (file-backed) with top-3 badges and player profile inspection.
- Retro pixel-inspired UI and animated reward floaters.
- React 19
- Vite 8
- Matter.js
- Socket.IO
- Express + Mongoose (MongoDB for clans)
- Node.js 18+ (or current LTS)
- npm
npm installnpm run devOptional env var for custom API target (frontend):
VITE_API_BASE_URL=""Leave it empty for local Vite proxy behavior.
npm run serverRun npm run dev and npm run server in separate terminals during development.
The frontend calls /api/* and is proxied to http://localhost:3001 in development.
Netlify hosts this frontend as static files only. The leaderboard API must be deployed separately (for example Render, Railway, Fly.io, or your own server).
Set a Netlify environment variable so the frontend points to your deployed backend:
VITE_API_BASE_URL=https://your-backend-domain.com
Then redeploy the site.
Also allow your Netlify site origin in backend CORS settings.
If you need to contain an incident quickly:
- Set
EMERGENCY_SHUTDOWN=trueon the backend to return503for every request. - Set
READ_ONLY_MODE=trueon the backend to keep reads available but blockPOST /api/leaderboard/submit.
PowerShell example:
$env:EMERGENCY_SHUTDOWN="true"
npm run serverOr keep the leaderboard visible while stopping new writes:
$env:READ_ONLY_MODE="true"
npm run serverIf the backend is deployed on Render, Railway, Fly.io, or similar, set the same environment variable in the host dashboard and redeploy/restart the service.
To make the leaderboard persistent globally (shared by all users and not tied to one local machine), run the server with a hosted MongoDB database.
Important for clans: Clan APIs require MongoDB mode (MONGODB_URI must be set). If MongoDB is not configured, leaderboard works in file mode but clans are disabled.
- Create a MongoDB Atlas cluster and database user.
- Add your app host/IP to Atlas network access.
- Set environment variable
MONGODB_URIbefore starting the server.
Recommended local setup:
- Create a
.envfile in the project root. - Add your Atlas URI:
MONGODB_URI="mongodb+srv://<username>:<password>@<cluster>/<database>?retryWrites=true&w=majority"- Load it in your terminal session before running the server (PowerShell):
Get-Content .env | ForEach-Object {
if ($_ -match '^\s*#' -or $_ -match '^\s*$') { return }
$name, $value = $_ -split '=', 2
$env:$name = $value.Trim('"')
}
npm run server.env is ignored by git in .gitignore.
PowerShell example:
$env:MONGODB_URI="mongodb+srv://<user>:<password>@<cluster>/<db>?retryWrites=true&w=majority"
npm run serverIf MONGODB_URI is not set, the server falls back to local file storage (server/data/leaderboard.json).
npm run buildnpm run previewsrc/
App.jsx # Main game logic, physics wiring, upgrades, UI
App.css # Retro styling and responsive layout
index.css # Global baseline styles
server/
index.js # Persistent leaderboard API
data/
leaderboard.json
public/
index.html
- Peg collisions can award bonus coins based on upgrade chance.
- Slots level up as they fill, improving payouts.
- Ball count is capped by owned balls.
- Submit your current profile to the global leaderboard from the
Leaderboardtab.
Base URL (dev): http://localhost:3001
GET /api/health- health checkGET /api/leaderboard?limit=50- ranked entries by coinsGET /api/leaderboard/:username- single player profile and rankPOST /api/leaderboard/submit- create/update a player entry
All clan mutation/read endpoints require:
- A valid leaderboard username (
usernamequery/body field) x-player-tokenheader (same token used for leaderboard ownership)
Main endpoints:
GET /api/clans?limit=60&search=name- clan leaderboard/search by scoreGET /api/clans/war-leaderboard- leaderboard ranked by clan war winsGET /api/clans/me?username=...- current player's clan + war notification statePOST /api/clans- create clan (multipart form:name,description,joinPermission,icon,username)POST /api/clans/:clanKey/join- join public clanGET /api/clans/:clanKey/home?username=...- clan home, war state, and war leaderboard snapshotGET /api/clans/:clanKey/chat?username=...&limit=120- clan chat historyPOST /api/clans/:clanKey/chat- send chat messagePOST /api/clans/war/progress- submit member totalCoins during active warPOST /api/clans/war/ack- acknowledge clan war notification
Socket.IO events:
- Client emits
clan:joinwith{ clanKey, username, token } - Server emits
clan:messagefor real-time chat updates
If you are deploying backend on Render and frontend separately (Netlify/Vercel/etc), set these manually:
MONGODB_URI: MongoDB Atlas connection stringCORS_ORIGINS: comma-separated frontend origins, e.g.https://your-frontend.com,https://www.your-frontend.com- Optional safety toggles:
READ_ONLY_MODE,EMERGENCY_SHUTDOWN
Required backend dependencies were added:
socket.iomulter
Frontend dependency added:
socket.io-client
Because clan icons are uploaded, icons are currently stored as base64 data URLs in MongoDB for portability on Render's ephemeral filesystem.
Submission ownership protection:
POST /api/leaderboard/submitnow requires a player ownership token.- Send token via
x-player-tokenheader (orownerTokenin JSON body). - New usernames are bound to the first valid token that creates them.
- Existing protected usernames can only be updated with the same token.
- Legacy usernames without a stored token are locked from updates until migrated.
Submission payload fields used by the app:
username(3-20 chars, letters/numbers/spaces/_/-)coinstotalCoinstotalBallsupgradesslotLevelsownedSkinsselectedSkinownerToken(64-char hex token, or pass inx-player-tokenheader)
Data persistence:
- Global mode: MongoDB Atlas (
MONGODB_URI) - Local fallback:
server/data/leaderboard.json - Entries are updated by username (case-insensitive)
Health endpoint output includes storage mode:
storage: "mongo"for globally persistent modestorage: "file"for local fallback mode
MIT License