Lucky Hand is a lightweight, real-time party game where players pick a side each round (Left or Right). The host randomly eliminates one side; survivors advance to the next round until a single winner remains. This repository is a monorepo containing both the backend (Node.js + Socket.IO) and the frontend (React + Vite + Socket.IO client).
- Backend:
backend/(Express + Socket.IO) - Frontend:
frontend/(React + Vite)
- Real-time gameplay using WebSockets (Socket.IO)
- Host creates a room and starts rounds
- Players join via a 6-character game ID
- Each round: players choose Left or Right
- Host eliminates a random side; eliminated users are tracked
- Game ends when 0 or 1 survivors remain; winner is announced
- Leaderboard based on elimination order
- Backend: Node.js, Express, Socket.IO
- Frontend: React 18, Vite, React Router, Socket.IO Client, Axios
- Containerization: Docker (per app), Nginx for serving built frontend
/backend
app.js # Socket.IO server, HTTP API (game + players)
gameLogic.js # Game state machine (status, rounds, winner)
utils.js # Helpers (generateGameId)
Dockerfile
docker-compose.yml
package.json
/frontend
src/
pages/ # EntryPage, GamePage
components/ # WaitingRoom, WaitingChoices, Elimination, GameOver, ListPlayers, CreateGame, JoinGame
Dockerfile # Multi-stage build; Nginx runtime
docker-compose.yml
vite.config.js
- A player creates a game with a name and their nickname. A 6-character
GAME_IDis generated. - Others join using
GAME_IDand their nicknames. - Host starts the game. Status transitions:
not_started→waiting_choices→elimination→ repeat … →ended
- In
waiting_choices, each player selects Left or Right. - Host triggers elimination; the server randomly picks the losing side and eliminates players on that side (and those who made no choice).
- When 0–1 active players remain, the game ends and a winner is declared (if exists). Rankings show elimination order; winner is last.
- Node.js 18+ (frontend uses Node 20 in Docker, but local dev is fine with 18+)
- npm
Frontend expects a backend base URL at runtime:
VITE_BACKEND_URL(e.g.,http://localhost:3000)
You can set it by creating frontend/.env:
VITE_BACKEND_URL=http://localhost:3000
cd backend
npm install
npm run dev # runs with nodemon on port 3000
Server logs: "Server is running on port 3000".
cd frontend
npm install
# Ensure VITE_BACKEND_URL is set as above
npm run dev # Vite dev server (default 5173)
Open http://localhost:5173 in the browser.
From the frontend, a single socket connection is established to VITE_BACKEND_URL with WebSocket transport only.
createGame{ gameName, playerName }joinGame{ gameId, playerName }leaveGame()startGame()# only the host can startmakeChoice{ choice }# choice ∈ {"left", "right"}eliminatePlayers()# only the host; performs random eliminationnextRound()# only the host; advances to next roundremoveGame()# only the host; deletes the game
deliverSocketInfo{ gameId, playerName, playerId }gameCreatedstringplayerJoinedstringplayerLeftstringgameStartedstringplayerMadeChoicestringeliminatedstring# sent to eliminated playergameEndedstringroundStartedstringrefreshGameInfo()errorstring
GET /games→ object of all in-memory games keyed bygameId.GET /games/:id→ game object{ id, name, createdBy, creatorId, status, round, winner, rankings }
GET /games/:id/players→ array of players currently in room[{ id, name, gameId, isEliminated }, ...]
GET /players→ array of all connected sockets with basic info
Notes:
- All game state is in-memory per process; no persistence.
- Game lifecycle is tied to WebSocket rooms; empty rooms are deleted.
EntryPage: Create or Join.WaitingRoom: Shows game name,GAME_ID, current players; host can Start.WaitingChoices: Shows round number; players choose; host can Eliminate.Elimination: Shows survived/eliminated lists; host can start Next Round.GameOver: Shows winner and a leaderboard derived fromrankings.
backend/Dockerfile builds a simple Node.js runtime.
- Exposes port
8080inside the container; the app listens on3000in code, so use port mapping accordingly in compose. - Example compose (
backend/docker-compose.yml) maps host127.0.0.1:4005 → container 3000.
Build and run locally:
cd backend
docker build -t luckyhand-api .
# Ensure port mapping to 3000 (app port)
docker run --rm -p 127.0.0.1:4005:3000 --name luckyhand-api luckyhand-api
Multi-stage build produces a static site served by Nginx.
cd frontend
docker build -t luckyhand-web .
docker run --rm -p 127.0.0.1:4006:80 --name luckyhand-web luckyhand-web
Access: http://127.0.0.1:4006
Set the frontend to point at the backend via env at build time or use a reverse proxy (recommended) to route /socket.io and /games* to the backend.
- Consider a reverse proxy (Nginx/Caddy) in front of both services (e.g.,
luckyhand.ituacm.comfor frontend and proxy/socket.io+/gamesto backend). - Configure CORS and Socket.IO
originto include production URL(s). Backend currently allows:http://127.0.0.1:5173,http://localhost:5173,https://luckyhand.ituacm.com. - Ensure WebSocket transport is enabled (already forced on both client and server).
- The backend keeps state in memory; use a single instance or implement shared state if scaling horizontally (e.g., Redis adapter for Socket.IO).
- Backend
Dockerfileexposes8080but the app listens on3000; remember to map the correct inner port. ListPlayersshows eitherplayer.name(objects) or the string itself (rankings list).- In development, ensure
VITE_BACKEND_URLmatches where the backend is reachable from the browser. - Error events from the server are surfaced via
alert()to aid debugging.
MIT (c) ITU ACM. See owners/maintainers for details.