diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..711668a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,73 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Multiplayer Tetris Battle Royale game with 2-8 player real-time battles via WebSocket. Players clear rows to send garbage to opponents; last player standing wins. + +## Commands + +```bash +# Install dependencies +cd server && npm install + +# Start server +cd server && npm start +# or +cd server && node index.js + +# Server runs on http://localhost:3000 (or PORT env variable) +``` + +## Architecture + +### Server-Client Model + +- **Server** (`server/index.js`): Express + Socket.io handles all game logic authoritatively + - Manages single global lobby with `lobby.players` Map + - Game tick runs at 50ms intervals via `gameTick()` + - Broadcasts state updates to all connected clients via `broadcastState()` + - Tetromino definitions and board constants are duplicated on server + +- **Client** (`public/js/`): Vanilla JavaScript with module pattern + - `network.js`: Socket.io client wrapper, manages player/game state caching + - `game.js`: `TetrisGame` class for local game state (mirrors server) + - `renderer.js`: `TetrisRenderer` class - Canvas rendering for all player boards + - `ui.js`: `UIManager` class - screen transitions, DOM manipulation + - `app.js`: Entry point, ties modules together, game loop via `requestAnimationFrame` + +### Game Flow + +1. **Lobby**: Players join global lobby via `join-lobby` socket event +2. **Ready**: All players click READY; game starts when all ready (2-8 players) +3. **Game**: Server authoritative - client inputs sent via socket, server broadcasts state +4. **Elimination**: Player eliminated when piece locks at top or garbage fills board +5. **Victory**: Game ends when 1 active player remains + +### Key Constants + +- `BOARD_WIDTH = 10`, `BOARD_HEIGHT = 20`, `CELL_SIZE = 24` +- `LOBBY_ROOM = 'global-lobby'` - Socket.io room for all players +- Garbage rules: 2 lines cleared -> 1 garbage row, 3 -> 2, 4 (Tetris) -> 4 rows + +### Socket Events + +| Event | Direction | Payload | +|-------|-----------|---------| +| `join-lobby` | Client->Server | `{ playerName }` | +| `ready` | Client->Server | - | +| `player-move` | Client->Server | `{ playerId, direction }` | +| `player-rotate` | Client->Server | `{ playerId }` | +| `player-drop` | Client->Server | `{ playerId, hard }` | +| `player-joined` | Server->Client | `{ player, players }` | +| `player-left` | Server->Client | `{ playerId, players }` | +| `game-started` | Server->Client | `{ players, states }` | +| `state-update` | Server->Client | `states[]` | +| `game-over` | Server->Client | `{ states }` | + +### Rendering + +- Each player gets a dynamically created board with canvas + info divs +- `renderer.setActivePlayer()` marks current player's board as `.main`, others as `.spectator` +- Battle grid layout classes: `.single-player`, `.two-players`, `.multi-player` diff --git a/public/css/style.css b/public/css/style.css index 56fb028..67bf86d 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -289,6 +289,19 @@ button:active { border: 1px solid #333; } +/* Hold and Next piece previews side by side */ +.board-info:has(canvas) { + justify-content: center; + gap: 20px; + padding: 8px 5px; +} + +.board-info canvas { + border: 1px solid #333; + background: #000; +} +} + #game-header { display: flex; justify-content: space-between; @@ -399,3 +412,111 @@ button:active { .player-board.flash { animation: flash 0.3s ease-in-out; } + +/* Touch Controls */ +#touch-controls { + display: none; + width: 100%; + max-width: 400px; + margin-top: 20px; + gap: 20px; + justify-content: center; + align-items: flex-end; + padding-bottom: 20px; +} + +#touch-dpad { + display: grid; + grid-template-columns: repeat(3, 70px); + grid-template-rows: repeat(2, 70px); + gap: 8px; +} + +#btn-left { + grid-column: 1; + grid-row: 2; +} + +#btn-down { + grid-column: 2; + grid-row: 2; +} + +#btn-right { + grid-column: 3; + grid-row: 2; +} + +#touch-actions { + display: grid; + grid-template-columns: repeat(2, 70px); + grid-template-rows: repeat(2, 70px); + gap: 8px; +} + +#btn-rotate { + grid-column: 1; + grid-row: 1; +} + +#btn-drop { + grid-column: 2; + grid-row: 1; +} + +#btn-hold { + grid-column: 1; + grid-row: 2; +} + +.touch-btn { + width: 70px; + height: 70px; + font-size: 1.5rem; + background: #333; + border: 3px solid #555; + border-radius: 10px; + padding: 0; + display: flex; + justify-content: center; + align-items: center; + user-select: none; + -webkit-user-select: none; + touch-action: manipulation; +} + +.touch-btn:active { + background: #0ff; + color: #000; + transform: scale(0.95); +} + +.touch-btn.action-btn { + background: #444; +} + +.touch-btn.drop-btn { + background: #ff00ff; + border-color: #ff00ff; +} + +.touch-btn.hold-btn { + background: #ff8800; + border-color: #ff8800; + font-size: 0.9rem; +} + +/* Show touch controls on mobile only */ +@media (max-width: 768px) and (hover: none) { + #touch-controls { + display: flex; + } + + #battle-grid { + height: 60vh; + } + + .player-board.spectator { + display: none; + } +} diff --git a/public/index.html b/public/index.html index f42d62d..3a38cdb 100644 --- a/public/index.html +++ b/public/index.html @@ -7,7 +7,7 @@ - +