From 5da603370437ced43b401b16a7a445c36b75284b Mon Sep 17 00:00:00 2001 From: Josue Zamudio Date: Fri, 20 Mar 2026 00:34:06 -0700 Subject: [PATCH] Initial commit: Tetris Battle Royale multiplayer game Features: - 2-8 player multiplayer via Socket.io WebSocket - Real-time board synchronization - all players see all boards - Battle royale mechanic: clearing rows sends garbage to opponents - Classic Tetris gameplay with all 7 tetrominoes - Retro visual styling with CRT scanlines and pixel font - Automatic level progression and speed increase - Player elimination and winner announcement Files: - server/index.js: Node.js + Socket.io game server - public/js/: Frontend game logic, rendering, network, and UI - public/css/style.css: Retro Tetris styling - README.md: Setup and usage instructions - PLAN.md: Implementation plan with all phases completed --- .gitignore | 3 + PLAN.md | 184 +++++++ README.md | 86 +++ public/css/style.css | 303 +++++++++++ public/index.html | 61 +++ public/js/app.js | 171 ++++++ public/js/game.js | 324 +++++++++++ public/js/network.js | 153 ++++++ public/js/renderer.js | 264 +++++++++ public/js/ui.js | 138 +++++ server/index.js | 401 ++++++++++++++ server/package-lock.json | 1100 ++++++++++++++++++++++++++++++++++++++ server/package.json | 15 + 13 files changed, 3203 insertions(+) create mode 100644 .gitignore create mode 100644 PLAN.md create mode 100644 README.md create mode 100644 public/css/style.css create mode 100644 public/index.html create mode 100644 public/js/app.js create mode 100644 public/js/game.js create mode 100644 public/js/network.js create mode 100644 public/js/renderer.js create mode 100644 public/js/ui.js create mode 100644 server/index.js create mode 100644 server/package-lock.json create mode 100644 server/package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c45938 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +*.log +.DS_Store diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..fe8ba33 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,184 @@ +# Tetris Battle Royale - Implementation Plan + +## Context + +Build a multiplayer Tetris Battle Royale game where 2-8 players compete. When a player clears rows, those rows are garbage-dumped onto random opponents' boards. Players see all other boards in real-time. Uses WebSocket for multiplayer and classic retro aesthetics. + +## Architecture + +### Tech Stack +- **Frontend**: HTML5 Canvas, CSS3, Vanilla JavaScript +- **Backend**: Node.js + Socket.io for real-time WebSocket communication +- **No frameworks** - pure vanilla implementation + +### File Structure +``` +tetris-battle-royale/ +├── server/ +│ ├── index.js # Socket.io server, game state management +│ └── package.json +├── public/ +│ ├── index.html # Game UI with player grids +│ ├── css/ +│ │ └── style.css # Retro pixel-art styling +│ └── js/ +│ ├── game.js # Core Tetris logic +│ ├── renderer.js # Canvas rendering +│ ├── network.js # Socket.io client +│ ├── ui.js # HUD and game messages +│ └── app.js # Main application +└── README.md +``` + +--- + +## Phase 1: Server Setup & Multiplayer Infrastructure [DONE] + +**Goal**: Get a working Node.js server with Socket.io that handles rooms and player connections. + +### Todos +- [x] Initialize Node.js project with `package.json` +- [x] Install dependencies: `express`, `socket.io`, `uuid` +- [x] Create basic Express server serving static files from `public/` +- [x] Implement Socket.io room system: + - `join-room` event - player joins with username + - `leave-room` event - player leaves + - Broadcast `player-joined` / `player-left` to room +- [x] Track players in room with IDs, names, ready status +- [x] Create simple HTML page to test connection + +**Success criteria**: Can open 2+ browser tabs, join same room, see player list update. + +--- + +## Phase 2: Core Tetris Game Logic [DONE] + +**Goal**: Implement single-player Tetris mechanics on the client side. + +### Todos +- [x] Create `public/js/game.js` with: + - 10x20 grid data structure + - Tetromino definitions (I, O, T, S, Z, J, L shapes and colors) + - Piece spawning at top center + - Movement: left, right, soft drop + - Rotation with wall kick (basic SRS) + - Hard drop (instant place) + - Row clearing detection + - Game over detection (piece collides at spawn) +- [x] Create `public/js/renderer.js` with: + - Canvas setup per player board + - Draw grid, active piece, locked pieces + - Classic Tetris colors for each piece type +- [x] Wire keyboard controls (arrow keys + space for hard drop) +- [x] Game loop with adjustable speed (starts slow, speeds up with lines) + +**Success criteria**: Single-player Tetris works smoothly with all piece types and controls. + +--- + +## Phase 3: Multiplayer Game State Sync [DONE] + +**Goal**: Connect multiple players and sync game state in real-time. + +### Todos +- [x] Server: Add game state tracking per room + - Active pieces, board states, scores + - Game running/paused/over status +- [x] Client `public/js/network.js`: + - Send player actions to server (`move-piece`, `rotate`, `drop`) + - Receive state updates and apply locally +- [x] Server: Broadcast state at ~20fps to all room players +- [x] Server: Handle `start-game` when 2+ players ready +- [x] Client: Use networked state +- [x] Sync piece positions, board state, scores across all players + +**Success criteria**: 2+ players can play simultaneously, all see each other's boards and piece movements in real-time. + +--- + +## Phase 4: Garbage Battle System [DONE] + +**Goal**: Implement the battle royale mechanic - clearing rows sends garbage to opponents. + +### Todos +- [x] Server: Track lines cleared per player +- [x] Implement garbage row calculation: + - 1 line → 0 garbage rows + - 2 lines → 1 garbage row to random opponent + - 3 lines → 2 garbage rows to random opponent(s) + - 4 lines (Tetris) → 4 garbage rows +- [x] Garbage row representation (filled row with 1 random gap) +- [x] Server: Pick random target(s) when garbage sent +- [x] Client: Receive garbage, shift board up, add garbage at bottom +- [x] Visual: Garbage rows have distinct color (gray/black) +- [x] Elimination: Player loses when garbage reaches top + +**Success criteria**: Clearing rows visibly adds garbage rows to opponent boards. + +--- + +## Phase 5: Retro UI Polish [IN PROGRESS] + +**Goal**: Classic Tetris aesthetic with multi-board layout. + +### Todos +- [x] `public/css/style.css`: + - Import Press Start 2P font from Google Fonts + - CSS Grid layout for player boards (adaptive 2x2 or 2x4) + - Retro color scheme (dark background, bright pieces) + - CRT scanline overlay effect + - Pixelated canvas rendering (`image-rendering: pixelated`) +- [x] `public/js/ui.js`: + - Player info HUD (name, score, lines) + - Player list sidebar with status (playing/eliminated) + - Room join screen with username input + - "Ready" button and countdown before game start + - Game over screen with winner announcement + - Next piece preview panel +- [ ] Visual effects: + - [x] Row clear flash animation + - [x] Garbage receiving shake effect + - [ ] Player color indicators per board + +**Success criteria**: Game looks like classic Tetris with all boards visible and polished UI. + +--- + +## Phase 6: Polish & Edge Cases [PENDING] + +**Goal**: Handle real-world scenarios and add final touches. + +### Todos +- [x] Player disconnect handling: + - Remove from room, update player list + - Continue game with remaining players or end if <2 +- [ ] Rejoin support (optional): Allow disconnected player to rejoin +- [x] Score tracking and leaderboard +- [x] Level system: Speed increases every 10 lines +- [ ] Combo system: Bonus for consecutive clears +- [ ] Sound effects (optional): Clear, garbage receive, game over +- [ ] Settings menu: Speed, sound toggle, controls +- [ ] README with setup instructions + +**Success criteria**: Game handles disconnections gracefully, full feature set working. + +--- + +## Dependencies +```json +{ + "dependencies": { + "express": "^4.18.2", + "socket.io": "^4.7.2", + "uuid": "^9.0.0" + } +} +``` + +## Verification + +1. `npm install` then `node server/index.js` +2. Open `http://localhost:3000` in 2-8 tabs +3. Join same room, click Ready, verify countdown +4. Play and verify garbage rows appear on opponents +5. Test disconnects, game over, winner announcement diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd26060 --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# Tetris Battle Royale + +A multiplayer Tetris Battle Royale game built with HTML, CSS, JavaScript, and Node.js. + +## Features + +- **2-8 Player Multiplayer**: Real-time battles via WebSocket +- **Battle Royale Mechanics**: Clear rows to send garbage to opponents +- **Classic Tetris Gameplay**: All 7 tetrominoes with standard rotation +- **Retro Visuals**: CRT scanlines, pixel art font, classic colors +- **Multiple Boards View**: See all players' boards simultaneously + +## How It Works + +When you clear rows, garbage rows are sent to random opponents: +- 2 lines cleared → 1 garbage row sent +- 3 lines cleared → 2 garbage rows sent +- 4 lines (Tetris) → 4 garbage rows sent + +The last player standing wins! + +## Setup + +1. Install dependencies: + ```bash + cd server + npm install + ``` + +2. Start the server: + ```bash + node index.js + ``` + +3. Open `http://localhost:3000` in 2-8 browser tabs + +4. Enter a room name and your name, then join + +5. Click "READY" when all players are ready + +## Controls + +- **Arrow Left/Right**: Move piece +- **Arrow Up**: Rotate piece +- **Arrow Down**: Soft drop +- **Space**: Hard drop + +## Game Flow + +1. **Lobby**: Players join a room and wait +2. **Ready**: All players click READY to start +3. **Game**: Play Tetris, clear rows to attack opponents +4. **Elimination**: Players are eliminated when pieces stack to the top +5. **Victory**: Last player standing wins + +## File Structure + +``` +tetris-battle-royale/ +├── server/ +│ ├── index.js # Node.js + Socket.io server +│ └── package.json +├── public/ +│ ├── index.html # Main HTML +│ ├── css/ +│ │ └── style.css # Retro styling with CRT effects +│ └── js/ +│ ├── game.js # Tetris game logic +│ ├── renderer.js # Canvas rendering +│ ├── network.js # Socket.io client +│ ├── ui.js # UI management +│ └── app.js # Main application +└── README.md +``` + +## Technology Stack + +- **Frontend**: Vanilla JavaScript, HTML5 Canvas, CSS3 +- **Backend**: Node.js, Express, Socket.io +- **No frameworks** - pure vanilla implementation + +## Troubleshooting + +- **Port already in use**: Change PORT in `server/index.js` +- **Can't connect**: Make sure server is running on port 3000 +- **Game not starting**: Need at least 2 players ready diff --git a/public/css/style.css b/public/css/style.css new file mode 100644 index 0000000..1058c6d --- /dev/null +++ b/public/css/style.css @@ -0,0 +1,303 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + background-color: #1a1a2e; + color: #fff; + font-family: 'Press Start 2P', cursive; + min-height: 100vh; + overflow-x: hidden; +} + +/* CRT Scanline Effect */ +body::before { + content: ""; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: repeating-linear-gradient( + 0deg, + rgba(0, 0, 0, 0.15), + rgba(0, 0, 0, 0.15) 1px, + transparent 1px, + transparent 2px + ); + pointer-events: none; + z-index: 1000; +} + +#app { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + padding: 20px; +} + +.screen { + display: none; + flex-direction: column; + align-items: center; + gap: 20px; + width: 100%; + max-width: 1200px; +} + +.screen.active { + display: flex; +} + +h1 { + font-size: 2rem; + text-align: center; + color: #0ff; + text-shadow: 4px 4px 0 #ff00ff; + margin-bottom: 30px; +} + +h2 { + font-size: 1.2rem; + color: #0f0; + text-shadow: 2px 2px 0 #004400; +} + +h3 { + font-size: 1rem; + color: #ff0; + text-shadow: 2px 2px 0 #444400; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 15px; +} + +label { + font-size: 0.7rem; + color: #888; +} + +input { + background: #0a0a1a; + border: 2px solid #333; + color: #fff; + font-family: 'Press Start 2P', cursive; + font-size: 0.8rem; + padding: 15px; + text-align: center; + outline: none; +} + +input:focus { + border-color: #0ff; +} + +button { + background: #ff00ff; + border: 4px solid #fff; + color: #fff; + font-family: 'Press Start 2P', cursive; + font-size: 1rem; + padding: 15px 30px; + cursor: pointer; + text-transform: uppercase; + transition: transform 0.1s; +} + +button:hover { + transform: scale(1.05); + background: #0ff; + color: #000; +} + +button:active { + transform: scale(0.95); +} + +#player-list { + display: flex; + flex-direction: column; + gap: 10px; + margin: 20px 0; + min-height: 150px; +} + +.player-item { + background: #0a0a1a; + border: 2px solid #333; + padding: 15px 20px; + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.8rem; +} + +.player-item .name { + color: #0ff; +} + +.player-item .status { + color: #888; +} + +.player-item .status.ready { + color: #0f0; +} + +/* Battle Grid - Responsive Layout */ +#battle-grid { + display: grid; + gap: 20px; + justify-content: center; + margin-top: 20px; +} + +#battle-grid.grid-2x2 { + grid-template-columns: repeat(2, 1fr); +} + +#battle-grid.grid-2x4 { + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(2, 1fr); +} + +.player-board { + background: #000; + border: 4px solid #333; + padding: 10px; + display: flex; + flex-direction: column; + align-items: center; + position: relative; +} + +.player-board.active { + border-color: #0ff; +} + +.player-board.eliminated { + border-color: #f00; + opacity: 0.5; +} + +.player-board canvas { + display: block; + image-rendering: pixelated; + image-rendering: crisp-edges; +} + +.board-info { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 5px; + font-size: 0.6rem; +} + +.board-info .name { + color: #0ff; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.board-info .stats { + color: #888; +} + +.next-piece-container { + display: flex; + align-items: center; + gap: 5px; +} + +.next-piece-container canvas { + border: 1px solid #333; +} + +#game-header { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + padding: 10px 20px; + background: #0a0a1a; + border: 2px solid #333; + margin-bottom: 20px; +} + +#game-header span { + font-size: 0.8rem; + color: #0f0; +} + +#game-header button { + font-size: 0.7rem; + padding: 10px 15px; +} + +#game-status { + margin-top: 20px; + padding: 15px; + background: #0a0a1a; + border: 2px solid #333; + text-align: center; + font-size: 0.8rem; + color: #ff0; +} + +#winner-display { + margin: 20px 0; + font-size: 1.2rem; +} + +#final-scores { + display: flex; + flex-direction: column; + gap: 10px; + margin: 20px 0; + width: 100%; + max-width: 400px; +} + +#final-scores .score-item { + background: #0a0a1a; + border: 2px solid #333; + padding: 10px 15px; + display: flex; + justify-content: space-between; +} + +#final-scores .score-item.winner { + border-color: #0f0; + color: #0f0; +} + +/* Garbage row animation */ +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-5px); } + 75% { transform: translateX(5px); } +} + +.player-board.shake { + animation: shake 0.2s ease-in-out; +} + +/* Flash effect for row clear */ +@keyframes flash { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.player-board.flash { + animation: flash 0.3s ease-in-out; +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..d81cf89 --- /dev/null +++ b/public/index.html @@ -0,0 +1,61 @@ + + + + + + Tetris Battle Royale + + + + + + +
+ +
+

TETRIS
BATTLE ROYALE

+
+ + +
+
+ + +
+ +
+ + +
+

Room:

+
+ +
+ + +
+
+ + +
+
+
+
+ + +
+

GAME OVER

+

+
+ +
+
+ + + + + + + + + diff --git a/public/js/app.js b/public/js/app.js new file mode 100644 index 0000000..dd34e8d --- /dev/null +++ b/public/js/app.js @@ -0,0 +1,171 @@ +// Main Application - Ties everything together + +let localGame = null; +let renderer = null; +let lastTime = 0; + +// Initialize when DOM is ready +document.addEventListener('DOMContentLoaded', () => { + // Connect to server + network.connect(); + + // Initialize renderer + renderer = new TetrisRenderer('battle-grid'); + + // Setup network listeners + setupNetworkListeners(); + + // Setup keyboard controls + setupKeyboardControls(); +}); + +function setupNetworkListeners() { + // Player joined lobby + network.setListener('player-joined', (player) => { + ui.updatePlayerList(network.getAllPlayers()); + }); + + // Player left lobby + network.setListener('player-left', (playerId) => { + ui.updatePlayerList(network.getAllPlayers()); + }); + + // Game started + network.setListener('game-started', (players, states) => { + ui.showScreen('game'); + ui.displays.gameRoomName.textContent = network.currentRoom; + + // Clear old boards + renderer.clearAll(); + + // Create boards for all players + states.forEach((state) => { + const player = network.getPlayer(state.playerId); + renderer.createPlayerBoard(state.playerId, player.name); + + // Initialize local game for current player + if (state.playerId === network.currentPlayerId) { + localGame = new TetrisGame(state.playerId); + localGame.loadState(state); + } + }); + + // Set up battle grid layout + updateBattleGridLayout(players.length); + + // Start game loop + lastTime = performance.now(); + requestAnimationFrame(gameLoop); + }); + + // State update during game + network.setListener('state-update', (states) => { + // Update local game if it's our state + const localState = states.find(s => s.playerId === network.currentPlayerId); + if (localState) { + localGame.loadState(localState); + } + + // Check for game over + const allStates = network.getAllGameStates(); + const activePlayers = Object.values(allStates).filter(s => !s.eliminated); + + if (activePlayers.length <= 1) { + endGame(allStates); + } + }); + + // Game over + network.setListener('game-over', (data) => { + endGame(data.states); + }); +} + +function setupKeyboardControls() { + document.addEventListener('keydown', (e) => { + if (ui.screens.game.classList.contains('active') && localGame) { + switch (e.key) { + case 'ArrowLeft': + network.sendMove('left'); + e.preventDefault(); + break; + case 'ArrowRight': + network.sendMove('right'); + e.preventDefault(); + break; + case 'ArrowDown': + network.sendDrop(); + e.preventDefault(); + break; + case 'ArrowUp': + network.sendRotate(); + e.preventDefault(); + break; + case ' ': + network.sendHardDrop(); + e.preventDefault(); + break; + } + } + }); +} + +function updateBattleGridLayout(playerCount) { + ui.displays.battleGrid.classList.remove('grid-2x2', 'grid-2x4'); + + if (playerCount <= 4) { + ui.displays.battleGrid.classList.add('grid-2x2'); + } else { + ui.displays.battleGrid.classList.add('grid-2x4'); + } +} + +function endGame(states) { + // Find winner + const activePlayers = Object.values(states).filter(s => !s.eliminated); + const eliminatedPlayers = Object.values(states).filter(s => s.eliminated); + + let winner = null; + let scores = {}; + + if (activePlayers.length === 1) { + const winnerState = activePlayers[0]; + const winnerPlayer = network.getPlayer(winnerState.playerId); + winner = winnerPlayer.name; + } + + // Build scores list + Object.values(states).forEach(state => { + const player = network.getPlayer(state.playerId); + if (player) { + scores[player.name] = state.score; + } + }); + + ui.showGameOver(winner, scores); +} + +function gameLoop(currentTime) { + if (!ui.screens.game.classList.contains('active')) { + return; + } + + const deltaTime = currentTime - lastTime; + lastTime = currentTime; + + // Update local game + if (localGame) { + localGame.update(deltaTime); + } + + // Render all players + const allStates = network.getAllGameStates(); + Object.values(allStates).forEach(state => { + renderer.renderPlayer(state.playerId, state); + }); + + // Set active player highlight + renderer.setActivePlayer(network.currentPlayerId); + + requestAnimationFrame(gameLoop); +} diff --git a/public/js/game.js b/public/js/game.js new file mode 100644 index 0000000..2529a63 --- /dev/null +++ b/public/js/game.js @@ -0,0 +1,324 @@ +// Core Tetris Game Logic + +const BOARD_WIDTH = 10; +const BOARD_HEIGHT = 20; +const CELL_SIZE = 24; + +// Tetromino definitions with colors +const TETROMINOS = { + I: { shape: [[0,0,0,0], [1,1,1,1], [0,0,0,0], [0,0,0,0]], color: '#00ffff' }, + O: { shape: [[1,1], [1,1]], color: '#ffff00' }, + T: { shape: [[0,1,0], [1,1,1], [0,0,0]], color: '#800080' }, + S: { shape: [[0,1,1], [1,1,0], [0,0,0]], color: '#00ff00' }, + Z: { shape: [[1,1,0], [0,1,1], [0,0,0]], color: '#ff0000' }, + J: { shape: [[1,0,0], [1,1,1], [0,0,0]], color: '#0000ff' }, + L: { shape: [[0,0,1], [1,1,1], [0,0,0]], color: '#ffa500' } +}; + +const TETROMINO_KEYS = Object.keys(TETROMINOS); +const GARbage_COLOR = '#666666'; + +class TetrisGame { + constructor(playerId) { + this.playerId = playerId; + this.board = this.createEmptyBoard(); + this.currentPiece = null; + this.nextPiece = null; + this.score = 0; + this.lines = 0; + this.level = 1; + this.gameOver = false; + this.eliminated = false; + + // Game speed (milliseconds per drop) + this.dropInterval = 1000; + this.lastDrop = 0; + + // Input lock for delay hits + this.inputLocked = false; + this.lockUntil = 0; + } + + createEmptyBoard() { + return Array(BOARD_HEIGHT).fill(null).map(() => Array(BOARD_WIDTH).fill(0)); + } + + initialize() { + this.board = this.createEmptyBoard(); + this.score = 0; + this.lines = 0; + this.level = 1; + this.gameOver = false; + this.eliminated = false; + this.dropInterval = 1000; + this.currentPiece = this.spawnPiece(); + this.nextPiece = this.getRandomPiece(); + + if (!this.currentPiece) { + this.gameOver = true; + } + } + + getRandomPiece() { + const key = TETROMINO_KEYS[Math.floor(Math.random() * TETROMINO_KEYS.length)]; + const tetromino = TETROMINOS[key]; + return { + type: key, + shape: JSON.parse(JSON.stringify(tetromino.shape)), + color: tetromino.color, + x: Math.floor(BOARD_WIDTH / 2) - Math.floor(tetromino.shape[0].length / 2), + y: 0 + }; + } + + spawnPiece() { + const piece = this.nextPiece; + this.nextPiece = this.getRandomPiece(); + + // Check if spawn position is valid + if (!this.isValidPosition(piece.x, piece.y, piece.shape)) { + return null; + } + + return piece; + } + + isValidPosition(x, y, shape) { + for (let row = 0; row < shape.length; row++) { + for (let col = 0; col < shape[row].length; col++) { + if (shape[row][col]) { + const newX = x + col; + const newY = y + row; + + // Check bounds + if (newX < 0 || newX >= BOARD_WIDTH || newY >= BOARD_HEIGHT) { + return false; + } + + // Check collision with locked pieces (only if on board) + if (newY >= 0 && this.board[newY][newX]) { + return false; + } + } + } + } + return true; + } + + move(direction) { + if (this.gameOver || this.inputLocked) return false; + + const newX = this.currentPiece.x + (direction === 'left' ? -1 : 1); + + if (this.isValidPosition(newX, this.currentPiece.y, this.currentPiece.shape)) { + this.currentPiece.x = newX; + return true; + } + return false; + } + + rotate() { + if (this.gameOver || this.inputLocked) return false; + + const originalShape = this.currentPiece.shape; + const rows = originalShape.length; + const cols = originalShape[0].length; + + // Rotate 90 degrees clockwise + const rotated = Array(cols).fill(null).map(() => Array(rows).fill(0)); + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + rotated[col][rows - 1 - row] = originalShape[row][col]; + } + } + + // Try rotation with wall kicks + const kicks = [0, -1, 1, -2, 2]; + for (const kick of kicks) { + if (this.isValidPosition(this.currentPiece.x + kick, this.currentPiece.y, rotated)) { + this.currentPiece.shape = rotated; + this.currentPiece.x += kick; + return true; + } + } + + return false; + } + + drop() { + if (this.gameOver || this.inputLocked) return false; + + const newY = this.currentPiece.y + 1; + + if (this.isValidPosition(this.currentPiece.x, newY, this.currentPiece.shape)) { + this.currentPiece.y = newY; + return true; + } + + // Lock the piece + return this.lockPiece(); + } + + hardDrop() { + if (this.gameOver || this.inputLocked) return 0; + + let dropped = 0; + while (this.isValidPosition(this.currentPiece.x, this.currentPiece.y + 1, this.currentPiece.shape)) { + this.currentPiece.y++; + dropped++; + } + + this.lockPiece(); + return dropped; + } + + lockPiece() { + if (!this.currentPiece) return false; + + // Lock piece into board + for (let row = 0; row < this.currentPiece.shape.length; row++) { + for (let col = 0; col < this.currentPiece.shape[row].length; col++) { + if (this.currentPiece.shape[row][col]) { + const boardY = this.currentPiece.y + row; + const boardX = this.currentPiece.x + col; + + if (boardY >= 0 && boardY < BOARD_HEIGHT && boardX >= 0 && boardX < BOARD_WIDTH) { + this.board[boardY][boardX] = this.currentPiece.color; + } + } + } + } + + // Check for game over (piece locked above visible area) + if (this.currentPiece.y <= 0) { + this.gameOver = true; + this.eliminated = true; + } + + // Clear completed rows + const cleared = this.clearRows(); + + // Spawn new piece + this.currentPiece = this.spawnPiece(); + if (!this.currentPiece) { + this.gameOver = true; + this.eliminated = true; + } + + return cleared; + } + + clearRows() { + let rowsCleared = 0; + + for (let row = BOARD_HEIGHT - 1; row >= 0; row--) { + if (this.board[row].every(cell => cell !== 0)) { + // Remove the row + this.board.splice(row, 1); + // Add empty row at top + this.board.unshift(Array(BOARD_WIDTH).fill(0)); + rowsCleared++; + row++; // Check same row again + } + } + + if (rowsCleared > 0) { + this.lines += rowsCleared; + this.updateScore(rowsCleared); + this.updateLevel(); + } + + return rowsCleared; + } + + updateScore(rowsCleared) { + const points = [0, 100, 300, 500, 800]; + this.score += points[rowsCleared] * this.level; + } + + updateLevel() { + this.level = Math.floor(this.lines / 10) + 1; + this.dropInterval = Math.max(100, 1000 - (this.level - 1) * 100); + } + + receiveGarbage(rows) { + if (this.gameOver) return; + + // Add garbage rows at bottom + for (let i = 0; i < rows; i++) { + // Remove top row (simulating overflow) + this.board.pop(); + + // Add garbage row with random gap + const garbageRow = Array(BOARD_WIDTH).fill(GARbage_COLOR); + const gap = Math.floor(Math.random() * BOARD_WIDTH); + garbageRow[gap] = 0; + this.board.unshift(garbageRow); + } + + // Check if garbage caused game over + for (let row = 0; row < 2; row++) { + for (let col = 0; col < BOARD_WIDTH; col++) { + if (this.board[row][col] !== 0) { + this.gameOver = true; + this.eliminated = true; + return; + } + } + } + } + + applyDelayHit(duration) { + this.inputLocked = true; + this.lockUntil = Date.now() + duration; + } + + update(deltaTime) { + if (this.gameOver) return; + + // Check if input lock has expired + if (this.inputLocked && Date.now() >= this.lockUntil) { + this.inputLocked = false; + } + + // Auto drop + if (!this.inputLocked && Date.now() - this.lastDrop >= this.dropInterval) { + this.drop(); + this.lastDrop = Date.now(); + } + } + + getState() { + return { + playerId: this.playerId, + board: JSON.parse(JSON.stringify(this.board)), + currentPiece: this.currentPiece ? JSON.parse(JSON.stringify(this.currentPiece)) : null, + nextPiece: this.nextPiece ? JSON.parse(JSON.stringify(this.nextPiece)) : null, + score: this.score, + lines: this.lines, + level: this.level, + gameOver: this.gameOver, + eliminated: this.eliminated + }; + } + + loadState(state) { + this.board = JSON.parse(JSON.stringify(state.board)); + this.currentPiece = state.currentPiece ? JSON.parse(JSON.stringify(state.currentPiece)) : null; + this.nextPiece = state.nextPiece ? JSON.parse(JSON.stringify(state.nextPiece)) : null; + this.score = state.score; + this.lines = state.lines; + this.level = state.level; + this.gameOver = state.gameOver; + this.eliminated = state.eliminated; + this.dropInterval = Math.max(100, 1000 - (this.level - 1) * 100); + } +} + +// Export for use in other modules +window.TetrisGame = TetrisGame; +window.TETROMINOS = TETROMINOS; +window.BOARD_WIDTH = BOARD_WIDTH; +window.BOARD_HEIGHT = BOARD_HEIGHT; +window.CELL_SIZE = CELL_SIZE; +window.GARBAGE_COLOR = GARbage_COLOR; diff --git a/public/js/network.js b/public/js/network.js new file mode 100644 index 0000000..2745363 --- /dev/null +++ b/public/js/network.js @@ -0,0 +1,153 @@ +// Network module - Socket.io client handling + +class NetworkManager { + constructor() { + this.socket = null; + this.currentRoom = null; + this.currentPlayerId = null; + this.players = {}; + this.gameState = {}; + this.listeners = { + onPlayerJoined: null, + onPlayerLeft: null, + onGameStarted: null, + onStateUpdate: null, + onGameOver: null + }; + } + + connect() { + this.socket = io(); + + this.socket.on('connect', () => { + console.log('Connected to server'); + this.currentPlayerId = this.socket.id; + }); + + this.socket.on('disconnect', () => { + console.log('Disconnected from server'); + this.currentRoom = null; + }); + + this.socket.on('player-joined', ({ player, players }) => { + this.updatePlayers(players); + if (this.listeners.onPlayerJoined) { + this.listeners.onPlayerJoined(player); + } + }); + + this.socket.on('player-left', ({ playerId, players }) => { + delete this.players[playerId]; + this.updatePlayers(players); + if (this.listeners.onPlayerLeft) { + this.listeners.onPlayerLeft(playerId); + } + }); + + this.socket.on('game-started', ({ players, states }) => { + this.updatePlayers(players); + this.updateStates(states); + if (this.listeners.onGameStarted) { + this.listeners.onGameStarted(players, states); + } + }); + + this.socket.on('state-update', (states) => { + this.updateStates(states); + if (this.listeners.onStateUpdate) { + this.listeners.onStateUpdate(states); + } + }); + + this.socket.on('game-over', (data) => { + if (this.listeners.onGameOver) { + this.listeners.onGameOver(data); + } + }); + } + + joinRoom(roomName, playerName) { + if (!this.socket) return; + + this.currentRoom = roomName; + this.socket.emit('join-room', { roomName, playerName }); + } + + leaveRoom() { + if (!this.socket) return; + + if (this.currentRoom) { + this.socket.leave(this.currentRoom); + } + this.currentRoom = null; + } + + ready() { + if (!this.socket) return; + this.socket.emit('ready'); + } + + sendMove(direction) { + if (!this.socket || !this.currentPlayerId) return; + this.socket.emit('player-move', { playerId: this.currentPlayerId, direction }); + } + + sendRotate() { + if (!this.socket || !this.currentPlayerId) return; + this.socket.emit('player-rotate', { playerId: this.currentPlayerId }); + } + + sendDrop() { + if (!this.socket || !this.currentPlayerId) return; + this.socket.emit('player-drop', { playerId: this.currentPlayerId, hard: false }); + } + + sendHardDrop() { + if (!this.socket || !this.currentPlayerId) return; + this.socket.emit('player-drop', { playerId: this.currentPlayerId, hard: true }); + } + + updatePlayers(players) { + this.players = {}; + players.forEach(p => { + this.players[p.id] = p; + }); + } + + updateStates(states) { + states.forEach(s => { + this.gameState[s.playerId] = s; + }); + } + + getPlayer(playerId) { + return this.players[playerId]; + } + + getGameState(playerId) { + return this.gameState[playerId]; + } + + getAllPlayers() { + return this.players; + } + + getAllGameStates() { + return this.gameState; + } + + setListener(event, callback) { + const listenerMap = { + 'player-joined': 'onPlayerJoined', + 'player-left': 'onPlayerLeft', + 'game-started': 'onGameStarted', + 'state-update': 'onStateUpdate', + 'game-over': 'onGameOver' + }; + if (callback) { + this.listeners[listenerMap[event]] = callback; + } + } +} + +const network = new NetworkManager(); diff --git a/public/js/renderer.js b/public/js/renderer.js new file mode 100644 index 0000000..f37ea94 --- /dev/null +++ b/public/js/renderer.js @@ -0,0 +1,264 @@ +// Canvas Renderer for Tetris + +class TetrisRenderer { + constructor(containerId) { + this.container = document.getElementById(containerId); + this.boards = new Map(); + } + + createPlayerBoard(playerId, playerName) { + const boardDiv = document.createElement('div'); + boardDiv.className = 'player-board'; + boardDiv.id = `board-${playerId}`; + boardDiv.dataset.playerId = playerId; + + // Main game canvas + const canvas = document.createElement('canvas'); + canvas.id = `canvas-${playerId}`; + canvas.width = BOARD_WIDTH * CELL_SIZE; + canvas.height = BOARD_HEIGHT * CELL_SIZE; + canvas.style.width = `${BOARD_WIDTH * CELL_SIZE}px`; + canvas.style.height = `${BOARD_HEIGHT * CELL_SIZE}px`; + boardDiv.appendChild(canvas); + + // Player info + const infoDiv = document.createElement('div'); + infoDiv.className = 'board-info'; + infoDiv.innerHTML = ` + ${playerName} + Lines: 0 + `; + boardDiv.appendChild(infoDiv); + + // Next piece preview + const nextDiv = document.createElement('div'); + nextDiv.className = 'board-info'; + const nextCanvas = document.createElement('canvas'); + nextCanvas.id = `next-${playerId}`; + nextCanvas.width = 80; + nextCanvas.height = 80; + nextCanvas.style.width = '80px'; + nextCanvas.style.height = '80px'; + nextDiv.innerHTML = 'NEXT:'; + nextDiv.appendChild(nextCanvas); + boardDiv.appendChild(nextDiv); + + this.container.appendChild(boardDiv); + + this.boards.set(playerId, { + element: boardDiv, + canvas: canvas, + nextCanvas: nextCanvas, + info: infoDiv, + ctx: canvas.getContext('2d'), + nextCtx: nextCanvas.getContext('2d') + }); + + return boardDiv; + } + + removePlayerBoard(playerId) { + const board = this.boards.get(playerId); + if (board) { + board.element.remove(); + this.boards.delete(playerId); + } + } + + renderPlayer(playerId, gameState) { + const boardData = this.boards.get(playerId); + if (!boardData) return; + + const { ctx, nextCtx, element, info } = boardData; + + // Clear canvas + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); + + // Draw grid lines + this.drawGrid(ctx); + + // Draw locked pieces + if (gameState && gameState.board) { + this.drawBoard(ctx, gameState.board); + } + + // Draw current piece + if (gameState && gameState.currentPiece && !gameState.gameOver) { + this.drawPiece(ctx, gameState.currentPiece); + } + + // Draw next piece + if (gameState && gameState.nextPiece) { + this.drawNextPiece(nextCtx, gameState.nextPiece); + } + + // Update stats + if (gameState) { + const linesSpan = info.querySelector('.lines'); + if (linesSpan) { + linesSpan.textContent = gameState.lines; + } + } + + // Update board state classes + element.classList.remove('active', 'eliminated'); + if (gameState && gameState.eliminated) { + element.classList.add('eliminated'); + } + } + + drawGrid(ctx) { + ctx.strokeStyle = '#222'; + ctx.lineWidth = 1; + + for (let x = 0; x <= BOARD_WIDTH; x++) { + ctx.beginPath(); + ctx.moveTo(x * CELL_SIZE, 0); + ctx.lineTo(x * CELL_SIZE, BOARD_HEIGHT * CELL_SIZE); + ctx.stroke(); + } + + for (let y = 0; y <= BOARD_HEIGHT; y++) { + ctx.beginPath(); + ctx.moveTo(0, y * CELL_SIZE); + ctx.lineTo(BOARD_WIDTH * CELL_SIZE, y * CELL_SIZE); + ctx.stroke(); + } + } + + drawBoard(ctx, board) { + for (let row = 0; row < BOARD_HEIGHT; row++) { + for (let col = 0; col < BOARD_WIDTH; col++) { + const color = board[row][col]; + if (color) { + this.drawCell(ctx, col, row, color); + } + } + } + } + + drawPiece(ctx, piece) { + for (let row = 0; row < piece.shape.length; row++) { + for (let col = 0; col < piece.shape[row].length; col++) { + if (piece.shape[row][col]) { + const x = piece.x + col; + const y = piece.y + row; + if (y >= 0) { + this.drawCell(ctx, x, y, piece.color); + } + } + } + } + } + + drawCell(ctx, x, y, color) { + const px = x * CELL_SIZE; + const py = y * CELL_SIZE; + + // Main fill + ctx.fillStyle = color; + ctx.fillRect(px, py, CELL_SIZE, CELL_SIZE); + + // Bevel effect + ctx.fillStyle = 'rgba(255,255,255,0.3)'; + ctx.fillRect(px, py, CELL_SIZE, 3); + ctx.fillRect(px, py, 3, CELL_SIZE); + + ctx.fillStyle = 'rgba(0,0,0,0.3)'; + ctx.fillRect(px, py + CELL_SIZE - 3, CELL_SIZE, 3); + ctx.fillRect(px + CELL_SIZE - 3, py, 3, CELL_SIZE); + + // Border + ctx.strokeStyle = 'rgba(0,0,0,0.5)'; + ctx.lineWidth = 1; + ctx.strokeRect(px, py, CELL_SIZE, CELL_SIZE); + } + + drawNextPiece(ctx, piece) { + // Clear + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); + + // Center the piece + const pieceWidth = piece.shape[0].length * 20; + const pieceHeight = piece.shape.length * 20; + const offsetX = (ctx.canvas.width - pieceWidth) / 2; + const offsetY = (ctx.canvas.height - pieceHeight) / 2; + + for (let row = 0; row < piece.shape.length; row++) { + for (let col = 0; col < piece.shape[row].length; col++) { + if (piece.shape[row][col]) { + const x = offsetX + col * 20; + const y = offsetY + row * 20; + + ctx.fillStyle = piece.color; + ctx.fillRect(x, y, 20, 20); + + ctx.fillStyle = 'rgba(255,255,255,0.3)'; + ctx.fillRect(x, y, 20, 3); + ctx.fillRect(x, y, 3, 20); + + ctx.fillStyle = 'rgba(0,0,0,0.3)'; + ctx.fillRect(x, y + 17, 20, 3); + ctx.fillRect(x + 17, y, 3, 20); + + ctx.strokeStyle = 'rgba(0,0,0,0.5)'; + ctx.lineWidth = 1; + ctx.strokeRect(x, y, 20, 20); + } + } + } + } + + setActivePlayer(playerId) { + // Remove active class from all boards + this.boards.forEach((boardData) => { + boardData.element.classList.remove('active'); + }); + + // Add to current player + const boardData = this.boards.get(playerId); + if (boardData) { + boardData.element.classList.add('active'); + } + } + + triggerShake(playerId) { + const boardData = this.boards.get(playerId); + if (boardData) { + boardData.element.classList.remove('shake'); + void boardData.element.offsetWidth; // Trigger reflow + boardData.element.classList.add('shake'); + } + } + + triggerFlash(playerId) { + const boardData = this.boards.get(playerId); + if (boardData) { + boardData.element.classList.remove('flash'); + void boardData.element.offsetWidth; // Trigger reflow + boardData.element.classList.add('flash'); + } + } + + updatePlayerInfo(playerId, name, score, lines) { + const boardData = this.boards.get(playerId); + if (boardData) { + const nameSpan = boardData.info.querySelector('.name'); + if (nameSpan) nameSpan.textContent = name; + + const linesSpan = boardData.info.querySelector('.lines'); + if (linesSpan) linesSpan.textContent = lines; + } + } + + clearAll() { + this.boards.forEach((boardData) => { + boardData.element.remove(); + }); + this.boards.clear(); + } +} + +window.TetrisRenderer = TetrisRenderer; diff --git a/public/js/ui.js b/public/js/ui.js new file mode 100644 index 0000000..b714d1b --- /dev/null +++ b/public/js/ui.js @@ -0,0 +1,138 @@ +// UI Module - Handle screens and user interactions + +class UIManager { + constructor() { + this.screens = { + room: document.getElementById('room-screen'), + lobby: document.getElementById('lobby-screen'), + game: document.getElementById('game-screen'), + gameover: document.getElementById('gameover-screen') + }; + + this.inputs = { + roomName: document.getElementById('room-name'), + playerName: document.getElementById('player-name') + }; + + this.buttons = { + join: document.getElementById('join-btn'), + ready: document.getElementById('ready-btn'), + leave: document.getElementById('leave-btn'), + backToLobby: document.getElementById('back-to-lobby') + }; + + this.displays = { + lobbyRoomName: document.getElementById('lobby-room-name'), + gameRoomName: document.getElementById('game-room-name'), + playerList: document.getElementById('player-list'), + battleGrid: document.getElementById('battle-grid'), + gameStatus: document.getElementById('game-status'), + winnerDisplay: document.getElementById('winner-display'), + finalScores: document.getElementById('final-scores') + }; + + this.bindEvents(); + } + + bindEvents() { + this.buttons.join.addEventListener('click', () => this.handleJoin()); + this.buttons.ready.addEventListener('click', () => this.handleReady()); + this.buttons.leave.addEventListener('click', () => this.handleLeave()); + this.buttons.backToLobby.addEventListener('click', () => this.handleBackToLobby()); + + // Allow Enter key to submit forms + this.inputs.roomName.addEventListener('keypress', (e) => { + if (e.key === 'Enter') this.handleJoin(); + }); + this.inputs.playerName.addEventListener('keypress', (e) => { + if (e.key === 'Enter') this.handleJoin(); + }); + } + + showScreen(screenName) { + Object.values(this.screens).forEach(screen => screen.classList.remove('active')); + this.screens[screenName].classList.add('active'); + } + + handleJoin() { + const roomName = this.inputs.roomName.value.trim(); + const playerName = this.inputs.playerName.value.trim(); + + if (!roomName || !playerName) { + this.showMessage('Please enter room name and your name'); + return; + } + + // Emit join event + network.setListener('player-joined', () => { + this.showScreen('lobby'); + this.displays.lobbyRoomName.textContent = roomName; + }); + + network.joinRoom(roomName, playerName); + } + + handleReady() { + network.ready(); + this.buttons.ready.textContent = 'READY!'; + this.buttons.ready.disabled = true; + } + + handleLeave() { + network.leaveRoom(); + this.showScreen('room'); + } + + handleBackToLobby() { + this.showScreen('room'); + } + + updatePlayerList(players) { + this.displays.playerList.innerHTML = ''; + Object.values(players).forEach(player => { + const item = document.createElement('div'); + item.className = 'player-item'; + const statusClass = player.ready ? 'ready' : ''; + item.innerHTML = ` + ${this.escapeHtml(player.name)} + ${player.ready ? 'READY' : 'WAITING'} + `; + this.displays.playerList.appendChild(item); + }); + } + + showMessage(message) { + this.displays.gameStatus.textContent = message; + setTimeout(() => { + this.displays.gameStatus.textContent = ''; + }, 3000); + } + + escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + showGameOver(winner, scores) { + this.displays.winnerDisplay.textContent = winner + ? `Winner: ${winner}!` + : 'Game Over!'; + this.displays.winnerDisplay.style.color = winner ? '#0f0' : '#fff'; + + this.displays.finalScores.innerHTML = ''; + Object.entries(scores).forEach(([name, score], index) => { + const item = document.createElement('div'); + item.className = 'score-item' + (index === 0 ? ' winner' : ''); + item.innerHTML = ` + ${this.escapeHtml(name)} + ${score} + `; + this.displays.finalScores.appendChild(item); + }); + + this.showScreen('gameover'); + } +} + +window.ui = new UIManager(); diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..779f904 --- /dev/null +++ b/server/index.js @@ -0,0 +1,401 @@ +const express = require('express'); +const http = require('http'); +const { Server } = require('socket.io'); +const path = require('path'); + +const app = express(); +const server = http.createServer(app); +const io = new Server(server); + +// Serve static files +app.use(express.static(path.join(__dirname, '../public'))); + +// Game state storage +const rooms = new Map(); + +const PORT = process.env.PORT || 3000; + +// Tetromino definitions +const TETROMINOS = { + I: { shape: [[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]], color: '#00ffff' }, + O: { shape: [[1,1],[1,1]], color: '#ffff00' }, + T: { shape: [[0,1,0],[1,1,1],[0,0,0]], color: '#800080' }, + S: { shape: [[0,1,1],[1,1,0],[0,0,0]], color: '#00ff00' }, + Z: { shape: [[1,1,0],[0,1,1],[0,0,0]], color: '#ff0000' }, + J: { shape: [[1,0,0],[1,1,1],[0,0,0]], color: '#0000ff' }, + L: { shape: [[0,0,1],[1,1,1],[0,0,0]], color: '#ffa500' } +}; + +const TETROMINO_KEYS = Object.keys(TETROMINOS); +const BOARD_WIDTH = 10; +const BOARD_HEIGHT = 20; +const GARBAGE_COLOR = '#666666'; + +io.on('connection', (socket) => { + console.log('Player connected:', socket.id); + + socket.on('join-room', ({ roomName, playerName }) => { + if (!rooms.has(roomName)) { + rooms.set(roomName, { + name: roomName, + players: new Map(), + gameStarted: false, + gameInterval: null + }); + } + + const room = rooms.get(roomName); + socket.join(roomName); + socket.data.roomName = roomName; + socket.data.playerName = playerName; + + const player = { + id: socket.id, + name: playerName, + score: 0, + lines: 0, + level: 1, + board: createEmptyBoard(), + currentPiece: null, + nextPiece: null, + eliminated: false, + ready: false, + dropCounter: 0, + dropInterval: 1000 + }; + + room.players.set(socket.id, player); + + io.to(roomName).emit('player-joined', { + player: { id: player.id, name: player.name }, + players: getPlayersList(room) + }); + + console.log(`${playerName} joined room ${roomName}`); + }); + + socket.on('ready', () => { + const roomName = socket.data.roomName; + if (!roomName) return; + + const room = rooms.get(roomName); + const player = room.players.get(socket.id); + player.ready = true; + + io.to(roomName).emit('player-joined', { + player: { id: player.id, name: player.name, ready: player.ready }, + players: getPlayersList(room) + }); + + if (room.players.size >= 2 && room.players.size <= 8) { + const allReady = Array.from(room.players.values()).every(p => p.ready); + if (allReady) { + startGame(room); + } + } + }); + + socket.on('player-move', ({ playerId, direction }) => { + const roomName = socket.data.roomName; + if (!roomName) return; + const room = rooms.get(roomName); + if (!room || !room.gameStarted) return; + + const player = room.players.get(playerId); + if (!player || player.eliminated) return; + + const newX = player.currentPiece.x + (direction === 'left' ? -1 : 1); + if (isValidPosition(player.currentPiece, newX, player.currentPiece.y, player.board)) { + player.currentPiece.x = newX; + broadcastState(room); + } + }); + + socket.on('player-rotate', ({ playerId }) => { + const roomName = socket.data.roomName; + if (!roomName) return; + const room = rooms.get(roomName); + if (!room || !room.gameStarted) return; + + const player = room.players.get(playerId); + if (!player || player.eliminated) return; + + const originalShape = player.currentPiece.shape; + const rows = originalShape.length; + const cols = originalShape[0].length; + + const rotated = Array(cols).fill(null).map(() => Array(rows).fill(0)); + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + rotated[col][rows - 1 - row] = originalShape[row][col]; + } + } + + const kicks = [0, -1, 1, -2, 2]; + for (const kick of kicks) { + if (isValidPosition(player.currentPiece, player.currentPiece.x + kick, player.currentPiece.y, player.board, rotated)) { + player.currentPiece.shape = rotated; + player.currentPiece.x += kick; + broadcastState(room); + return; + } + } + }); + + socket.on('player-drop', ({ playerId, hard }) => { + const roomName = socket.data.roomName; + if (!roomName) return; + const room = rooms.get(roomName); + if (!room || !room.gameStarted) return; + + const player = room.players.get(playerId); + if (!player || player.eliminated) return; + + if (hard) { + let dropped = 0; + while (isValidPosition(player.currentPiece, player.currentPiece.x, player.currentPiece.y + 1, player.board)) { + player.currentPiece.y++; + dropped++; + } + player.score += dropped * 2; + lockPiece(room, player); + } else { + const newY = player.currentPiece.y + 1; + if (isValidPosition(player.currentPiece, player.currentPiece.x, newY, player.board)) { + player.currentPiece.y = newY; + player.score += 1; + broadcastState(room); + } else { + lockPiece(room, player); + } + } + }); + + socket.on('disconnect', () => { + const roomName = socket.data.roomName; + if (!roomName) return; + + const room = rooms.get(roomName); + if (room) { + const player = room.players.get(socket.id); + if (player) { + console.log(`${player.name} left room ${roomName}`); + + if (room.gameStarted) { + player.eliminated = true; + broadcastState(room); + checkGameOver(room); + } + + room.players.delete(socket.id); + io.to(roomName).emit('player-left', { + playerId: socket.id, + players: getPlayersList(room) + }); + + if (room.players.size === 0) { + if (room.gameInterval) clearInterval(room.gameInterval); + rooms.delete(roomName); + } + } + } + }); +}); + +function createEmptyBoard() { + return Array(BOARD_HEIGHT).fill(null).map(() => Array(BOARD_WIDTH).fill(0)); +} + +function getPlayersList(room) { + return Array.from(room.players.values()).map(p => ({ + id: p.id, + name: p.name, + score: p.score, + lines: p.lines, + level: p.level, + eliminated: p.eliminated, + ready: p.ready + })); +} + +function isValidPosition(piece, x, y, board, shape = null) { + const s = shape || piece.shape; + for (let row = 0; row < s.length; row++) { + for (let col = 0; col < s[row].length; col++) { + if (s[row][col]) { + const newX = x + col; + const newY = y + row; + if (newX < 0 || newX >= BOARD_WIDTH || newY >= BOARD_HEIGHT) return false; + if (newY >= 0 && board[newY][newX]) return false; + } + } + } + return true; +} + +function getRandomPiece() { + const key = TETROMINO_KEYS[Math.floor(Math.random() * TETROMINO_KEYS.length)]; + const tetromino = TETROMINOS[key]; + return { + type: key, + shape: JSON.parse(JSON.stringify(tetromino.shape)), + color: tetromino.color, + x: Math.floor(BOARD_WIDTH / 2) - Math.floor(tetromino.shape[0].length / 2), + y: 0 + }; +} + +function spawnPiece(player) { + player.currentPiece = player.nextPiece; + player.nextPiece = getRandomPiece(); + return isValidPosition(player.currentPiece, player.currentPiece.x, player.currentPiece.y, player.board); +} + +function lockPiece(room, player) { + if (!player.currentPiece) return; + + for (let row = 0; row < player.currentPiece.shape.length; row++) { + for (let col = 0; col < player.currentPiece.shape[row].length; col++) { + if (player.currentPiece.shape[row][col]) { + const boardY = player.currentPiece.y + row; + const boardX = player.currentPiece.x + col; + if (boardY >= 0 && boardY < BOARD_HEIGHT && boardX >= 0 && boardX < BOARD_WIDTH) { + player.board[boardY][boardX] = player.currentPiece.color; + } + } + } + } + + if (player.currentPiece.y <= 0) player.eliminated = true; + + const rowsCleared = clearRows(player); + + if (!spawnPiece(player)) player.eliminated = true; + + if (rowsCleared > 0) sendGarbage(room, player, rowsCleared); + + broadcastState(room); + checkGameOver(room); +} + +function clearRows(player) { + let rowsCleared = 0; + for (let row = BOARD_HEIGHT - 1; row >= 0; row--) { + if (player.board[row].every(cell => cell !== 0)) { + player.board.splice(row, 1); + player.board.unshift(Array(BOARD_WIDTH).fill(0)); + rowsCleared++; + row++; + } + } + if (rowsCleared > 0) { + player.lines += rowsCleared; + const points = [0, 100, 300, 500, 800]; + player.score += points[rowsCleared] * player.level; + player.level = Math.floor(player.lines / 10) + 1; + player.dropInterval = Math.max(100, 1000 - (player.level - 1) * 100); + } + return rowsCleared; +} + +function sendGarbage(room, sender, rowsCleared) { + const garbageRows = rowsCleared >= 4 ? rowsCleared : Math.max(1, rowsCleared - 1); + const opponents = Array.from(room.players.values()).filter(p => p.id !== sender.id && !p.eliminated); + if (opponents.length === 0) return; + for (let i = 0; i < garbageRows; i++) { + const target = opponents[Math.floor(Math.random() * opponents.length)]; + addGarbageToPlayer(target); + } +} + +function addGarbageToPlayer(player) { + player.board.pop(); + const garbageRow = Array(BOARD_WIDTH).fill(GARBAGE_COLOR); + garbageRow[Math.floor(Math.random() * BOARD_WIDTH)] = 0; + player.board.unshift(garbageRow); + + for (let row = 0; row < 2; row++) { + for (let col = 0; col < BOARD_WIDTH; col++) { + if (player.board[row][col] !== 0) { + player.eliminated = true; + return; + } + } + } +} + +function startGame(room) { + room.gameStarted = true; + + for (const player of room.players.values()) { + player.board = createEmptyBoard(); + player.score = 0; + player.lines = 0; + player.level = 1; + player.eliminated = false; + player.dropInterval = 1000; + player.currentPiece = getRandomPiece(); + player.nextPiece = getRandomPiece(); + } + + room.gameInterval = setInterval(() => gameTick(room), 50); + + io.to(room.name).emit('game-started', { + players: getPlayersList(room), + states: getStates(room) + }); + + console.log(`Game started in room ${room.name} with ${room.players.size} players`); +} + +function gameTick(room) { + for (const player of room.players.values()) { + if (player.eliminated) continue; + + player.dropCounter += 50; + if (player.dropCounter >= player.dropInterval) { + player.dropCounter = 0; + if (isValidPosition(player.currentPiece, player.currentPiece.x, player.currentPiece.y + 1, player.board)) { + player.currentPiece.y++; + } else { + lockPiece(room, player); + } + } + } + broadcastState(room); + checkGameOver(room); +} + +function broadcastState(room) { + io.to(room.name).emit('state-update', getStates(room)); +} + +function getStates(room) { + return Array.from(room.players.values()).map(p => ({ + playerId: p.id, + board: JSON.parse(JSON.stringify(p.board)), + currentPiece: p.currentPiece ? JSON.parse(JSON.stringify(p.currentPiece)) : null, + nextPiece: p.nextPiece ? JSON.parse(JSON.stringify(p.nextPiece)) : null, + score: p.score, + lines: p.lines, + level: p.level, + eliminated: p.eliminated + })); +} + +function checkGameOver(room) { + const activePlayers = Array.from(room.players.values()).filter(p => !p.eliminated); + if (activePlayers.length <= 1) { + io.to(room.name).emit('game-over', { states: getStates(room) }); + if (room.gameInterval) { + clearInterval(room.gameInterval); + room.gameInterval = null; + } + room.gameStarted = false; + } +} + +server.listen(PORT, () => { + console.log(`Tetris Battle Royale server running on port ${PORT}`); + console.log(`Open http://localhost:${PORT} in 2-8 browser tabs to play!`); +}); diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 0000000..b02a82c --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,1100 @@ +{ + "name": "tetris-battle-royale", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tetris-battle-royale", + "version": "1.0.0", + "dependencies": { + "express": "^4.18.2", + "socket.io": "^4.7.2", + "uuid": "^9.0.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io": { + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.6.tgz", + "integrity": "sha512-U2SN0w3OpjFRVlrc17E6TMDmH58Xl9rai1MblNjAdwWp07Kk+llmzX0hjDpQdrDGzwmvOtgM5yI+meYX6iZ2xA==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", + "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.18.3" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..c1c8a66 --- /dev/null +++ b/server/package.json @@ -0,0 +1,15 @@ +{ + "name": "tetris-battle-royale", + "version": "1.0.0", + "description": "Multiplayer Tetris Battle Royale", + "main": "index.js", + "scripts": { + "start": "node index.js", + "dev": "node index.js" + }, + "dependencies": { + "express": "^4.18.2", + "socket.io": "^4.7.2", + "uuid": "^9.0.0" + } +}