Refactor to single global lobby

Changes:
- Removed room-based architecture, now using single global lobby
- Players only need to enter their name to join
- Game starts when all players in lobby are ready (min 2, max 8)
- Simplified UI - no room name field, shows "Global Lobby" header
- Updated all server events to use io.emit instead of io.to(roomName)

Files modified:
- server/index.js: Replaced rooms Map with single lobby object
- public/index.html: Removed room name input, updated button text
- public/js/network.js: Renamed joinRoom/leaveRoom to joinLobby/leaveLobby
- public/js/ui.js: Simplified join flow, removed room name validation
- public/js/app.js: Updated game header to show "GLOBAL LOBBY"
- PLAN.md: Marked all phases as complete
This commit is contained in:
2026-03-20 07:09:51 -07:00
parent 5da6033704
commit e7917a338e
6 changed files with 172 additions and 274 deletions
+78 -141
View File
@@ -1,184 +1,121 @@
# Tetris Battle Royale - Implementation Plan # Single Global Lobby - Implementation Plan
## Context ## 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. Changed from room-based architecture to a single global lobby where:
- No room name input needed
## Architecture - All players join the same lobby
- Player list shows everyone in the lobby
### Tech Stack - Game starts when all players are ready (minimum 2 players)
- **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] ## Phase 1: Server - Replace Rooms with Single Lobby [DONE]
**Goal**: Get a working Node.js server with Socket.io that handles rooms and player connections. **Goal**: Remove room-based architecture and use a single global lobby.
### Todos ### Todos
- [x] Initialize Node.js project with `package.json` - [x] Replace `rooms` Map with single `lobby` object
- [x] Install dependencies: `express`, `socket.io`, `uuid` - [x] Change `join-room` event to `join-lobby`
- [x] Create basic Express server serving static files from `public/` - Remove roomName parameter
- [x] Implement Socket.io room system: - Only accept playerName
- `join-room` event - player joins with username - Add player to global lobby
- `leave-room` event - player leaves - [x] Update `ready` event
- Broadcast `player-joined` / `player-left` to room - Check if all players in global lobby are ready
- [x] Track players in room with IDs, names, ready status - Start game when all ready (min 2 players, max 8)
- [x] Create simple HTML page to test connection - [x] Update disconnect handling
- Remove from global lobby
- Handle game-in-progress disconnections
- [x] Update all broadcast functions
- Remove room name parameter
- Broadcast to all connected clients (io.emit instead of io.to(roomName).emit)
**Success criteria**: Can open 2+ browser tabs, join same room, see player list update. **Files modified**: `server/index.js`
**Success criteria**: Server uses single lobby, no room names needed.
--- ---
## Phase 2: Core Tetris Game Logic [DONE] ## Phase 2: Frontend HTML - Remove Room Name Input [DONE]
**Goal**: Implement single-player Tetris mechanics on the client side. **Goal**: Simplify the login screen to only require player name.
### Todos ### Todos
- [x] Create `public/js/game.js` with: - [x] Remove "Room Name" input field from login screen
- 10x20 grid data structure - [x] Remove room name label
- Tetromino definitions (I, O, T, S, Z, J, L shapes and colors) - [x] Change button text from "JOIN ROOM" to "JOIN LOBBY"
- Piece spawning at top center - [x] Update lobby screen header
- Movement: left, right, soft drop - Remove room name display
- Rotation with wall kick (basic SRS) - Show "Global Lobby" instead
- 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. **Files modified**: `public/index.html`
**Success criteria**: Login screen only asks for player name.
--- ---
## Phase 3: Multiplayer Game State Sync [DONE] ## Phase 3: Network Module - Update Lobby Methods [DONE]
**Goal**: Connect multiple players and sync game state in real-time. **Goal**: Update client-side network methods for single lobby architecture.
### Todos ### Todos
- [x] Server: Add game state tracking per room - [x] Rename `joinRoom` to `joinLobby`
- Active pieces, board states, scores - Remove roomName parameter
- Game running/paused/over status - Emit `join-lobby` event with just playerName
- [x] Client `public/js/network.js`: - [x] Rename `leaveRoom` to `leaveLobby`
- Send player actions to server (`move-piece`, `rotate`, `drop`) - Remove room name handling
- Receive state updates and apply locally - [x] Remove `currentRoom` property (no longer needed)
- [x] Server: Broadcast state at ~20fps to all room players - [x] Update all event listeners to work without room names
- [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. **Files modified**: `public/js/network.js`
**Success criteria**: Network module works with single lobby.
--- ---
## Phase 4: Garbage Battle System [DONE] ## Phase 4: UI Module - Simplify Join Flow [DONE]
**Goal**: Implement the battle royale mechanic - clearing rows sends garbage to opponents. **Goal**: Update UI handling for simplified lobby flow.
### Todos ### Todos
- [x] Server: Track lines cleared per player - [x] Update `handleJoin` method
- [x] Implement garbage row calculation: - Remove roomName input validation
- 1 line → 0 garbage rows - Only validate playerName
- 2 lines → 1 garbage row to random opponent - Call `network.joinLobby(playerName)`
- 3 lines → 2 garbage rows to random opponent(s) - [x] Update lobby display
- 4 lines (Tetris) → 4 garbage rows - Show "Global Lobby" instead of room name
- [x] Garbage row representation (filled row with 1 random gap) - Update player list display
- [x] Server: Pick random target(s) when garbage sent - [x] Update game over "Back to Lobby" flow
- [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. **Files modified**: `public/js/ui.js`
**Success criteria**: UI works with single lobby flow.
--- ---
## Phase 5: Retro UI Polish [IN PROGRESS] ## Phase 5: App Module - Update Event Handling [DONE]
**Goal**: Classic Tetris aesthetic with multi-board layout. **Goal**: Update main application to work with global lobby events.
### Todos ### Todos
- [x] `public/css/style.css`: - [x] Update network listener setup
- Import Press Start 2P font from Google Fonts - Adapt to new event structure (no room names)
- CSS Grid layout for player boards (adaptive 2x2 or 2x4) - [x] Update game start handling
- Retro color scheme (dark background, bright pieces) - Works with global lobby players
- CRT scanline overlay effect - [x] Update state sync for single lobby
- 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. **Files modified**: `public/js/app.js`
**Success criteria**: Game syncs properly with single lobby.
--- ---
## 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 ## Verification
1. `npm install` then `node server/index.js` 1. Start server: `cd server && node index.js`
2. Open `http://localhost:3000` in 2-8 tabs 2. Open `http://localhost:3000` in 2+ browser tabs
3. Join same room, click Ready, verify countdown 3. Enter only your name (no room name field exists)
4. Play and verify garbage rows appear on opponents 4. Click "JOIN LOBBY"
5. Test disconnects, game over, winner announcement 5. All players should see each other in the same "Global Lobby"
6. All players click READY
7. Game starts automatically when all players are ready (minimum 2)
+3 -7
View File
@@ -11,23 +11,19 @@
</head> </head>
<body> <body>
<div id="app"> <div id="app">
<!-- Room Selection Screen --> <!-- Login Screen -->
<div id="room-screen" class="screen active"> <div id="room-screen" class="screen active">
<h1>TETRIS<br>BATTLE ROYALE</h1> <h1>TETRIS<br>BATTLE ROYALE</h1>
<div class="form-group">
<label for="room-name">Room Name</label>
<input type="text" id="room-name" placeholder="Enter room name">
</div>
<div class="form-group"> <div class="form-group">
<label for="player-name">Your Name</label> <label for="player-name">Your Name</label>
<input type="text" id="player-name" placeholder="Enter your name"> <input type="text" id="player-name" placeholder="Enter your name">
</div> </div>
<button id="join-btn">JOIN ROOM</button> <button id="join-btn">JOIN LOBBY</button>
</div> </div>
<!-- Lobby Screen --> <!-- Lobby Screen -->
<div id="lobby-screen" class="screen"> <div id="lobby-screen" class="screen">
<h2>Room: <span id="lobby-room-name"></span></h2> <h2>GLOBAL LOBBY</h2>
<div id="player-list"></div> <div id="player-list"></div>
<button id="ready-btn">READY</button> <button id="ready-btn">READY</button>
</div> </div>
+1 -1
View File
@@ -33,7 +33,7 @@ function setupNetworkListeners() {
// Game started // Game started
network.setListener('game-started', (players, states) => { network.setListener('game-started', (players, states) => {
ui.showScreen('game'); ui.showScreen('game');
ui.displays.gameRoomName.textContent = network.currentRoom; ui.displays.gameRoomName.textContent = 'GLOBAL LOBBY';
// Clear old boards // Clear old boards
renderer.clearAll(); renderer.clearAll();
+3 -12
View File
@@ -3,7 +3,6 @@
class NetworkManager { class NetworkManager {
constructor() { constructor() {
this.socket = null; this.socket = null;
this.currentRoom = null;
this.currentPlayerId = null; this.currentPlayerId = null;
this.players = {}; this.players = {};
this.gameState = {}; this.gameState = {};
@@ -26,7 +25,6 @@ class NetworkManager {
this.socket.on('disconnect', () => { this.socket.on('disconnect', () => {
console.log('Disconnected from server'); console.log('Disconnected from server');
this.currentRoom = null;
}); });
this.socket.on('player-joined', ({ player, players }) => { this.socket.on('player-joined', ({ player, players }) => {
@@ -66,20 +64,13 @@ class NetworkManager {
}); });
} }
joinRoom(roomName, playerName) { joinLobby(playerName) {
if (!this.socket) return; if (!this.socket) return;
this.socket.emit('join-lobby', { playerName });
this.currentRoom = roomName;
this.socket.emit('join-room', { roomName, playerName });
} }
leaveRoom() { leaveLobby() {
if (!this.socket) return; if (!this.socket) return;
if (this.currentRoom) {
this.socket.leave(this.currentRoom);
}
this.currentRoom = null;
} }
ready() { ready() {
+5 -12
View File
@@ -10,7 +10,6 @@ class UIManager {
}; };
this.inputs = { this.inputs = {
roomName: document.getElementById('room-name'),
playerName: document.getElementById('player-name') playerName: document.getElementById('player-name')
}; };
@@ -22,7 +21,6 @@ class UIManager {
}; };
this.displays = { this.displays = {
lobbyRoomName: document.getElementById('lobby-room-name'),
gameRoomName: document.getElementById('game-room-name'), gameRoomName: document.getElementById('game-room-name'),
playerList: document.getElementById('player-list'), playerList: document.getElementById('player-list'),
battleGrid: document.getElementById('battle-grid'), battleGrid: document.getElementById('battle-grid'),
@@ -41,9 +39,6 @@ class UIManager {
this.buttons.backToLobby.addEventListener('click', () => this.handleBackToLobby()); this.buttons.backToLobby.addEventListener('click', () => this.handleBackToLobby());
// Allow Enter key to submit forms // Allow Enter key to submit forms
this.inputs.roomName.addEventListener('keypress', (e) => {
if (e.key === 'Enter') this.handleJoin();
});
this.inputs.playerName.addEventListener('keypress', (e) => { this.inputs.playerName.addEventListener('keypress', (e) => {
if (e.key === 'Enter') this.handleJoin(); if (e.key === 'Enter') this.handleJoin();
}); });
@@ -55,21 +50,19 @@ class UIManager {
} }
handleJoin() { handleJoin() {
const roomName = this.inputs.roomName.value.trim();
const playerName = this.inputs.playerName.value.trim(); const playerName = this.inputs.playerName.value.trim();
if (!roomName || !playerName) { if (!playerName) {
this.showMessage('Please enter room name and your name'); this.showMessage('Please enter your name');
return; return;
} }
// Emit join event // Set listener for player joined event
network.setListener('player-joined', () => { network.setListener('player-joined', () => {
this.showScreen('lobby'); this.showScreen('lobby');
this.displays.lobbyRoomName.textContent = roomName;
}); });
network.joinRoom(roomName, playerName); network.joinLobby(playerName);
} }
handleReady() { handleReady() {
@@ -79,7 +72,7 @@ class UIManager {
} }
handleLeave() { handleLeave() {
network.leaveRoom(); network.leaveLobby();
this.showScreen('room'); this.showScreen('room');
} }
+82 -101
View File
@@ -10,8 +10,12 @@ const io = new Server(server);
// Serve static files // Serve static files
app.use(express.static(path.join(__dirname, '../public'))); app.use(express.static(path.join(__dirname, '../public')));
// Game state storage // Single global lobby
const rooms = new Map(); const lobby = {
players: new Map(),
gameStarted: false,
gameInterval: null
};
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
@@ -34,19 +38,8 @@ const GARBAGE_COLOR = '#666666';
io.on('connection', (socket) => { io.on('connection', (socket) => {
console.log('Player connected:', socket.id); console.log('Player connected:', socket.id);
socket.on('join-room', ({ roomName, playerName }) => { // Join global lobby
if (!rooms.has(roomName)) { socket.on('join-lobby', ({ playerName }) => {
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; socket.data.playerName = playerName;
const player = { const player = {
@@ -64,60 +57,55 @@ io.on('connection', (socket) => {
dropInterval: 1000 dropInterval: 1000
}; };
room.players.set(socket.id, player); lobby.players.set(socket.id, player);
io.to(roomName).emit('player-joined', { // Broadcast to all clients
io.emit('player-joined', {
player: { id: player.id, name: player.name }, player: { id: player.id, name: player.name },
players: getPlayersList(room) players: getPlayersList()
}); });
console.log(`${playerName} joined room ${roomName}`); console.log(`${playerName} joined global lobby (${lobby.players.size} players)`);
}); });
socket.on('ready', () => { socket.on('ready', () => {
const roomName = socket.data.roomName; const player = lobby.players.get(socket.id);
if (!roomName) return; if (!player) return;
const room = rooms.get(roomName);
const player = room.players.get(socket.id);
player.ready = true; player.ready = true;
io.to(roomName).emit('player-joined', { // Broadcast updated player list
io.emit('player-joined', {
player: { id: player.id, name: player.name, ready: player.ready }, player: { id: player.id, name: player.name, ready: player.ready },
players: getPlayersList(room) players: getPlayersList()
}); });
if (room.players.size >= 2 && room.players.size <= 8) { // Check if all players ready and min 2 players
const allReady = Array.from(room.players.values()).every(p => p.ready); if (lobby.players.size >= 2 && lobby.players.size <= 8) {
const allReady = Array.from(lobby.players.values()).every(p => p.ready);
if (allReady) { if (allReady) {
startGame(room); startGame();
} }
} }
}); });
socket.on('player-move', ({ playerId, direction }) => { socket.on('player-move', ({ playerId, direction }) => {
const roomName = socket.data.roomName; if (!lobby.gameStarted) return;
if (!roomName) return;
const room = rooms.get(roomName);
if (!room || !room.gameStarted) return;
const player = room.players.get(playerId); const player = lobby.players.get(playerId);
if (!player || player.eliminated) return; if (!player || player.eliminated) return;
const newX = player.currentPiece.x + (direction === 'left' ? -1 : 1); const newX = player.currentPiece.x + (direction === 'left' ? -1 : 1);
if (isValidPosition(player.currentPiece, newX, player.currentPiece.y, player.board)) { if (isValidPosition(player.currentPiece, newX, player.currentPiece.y, player.board)) {
player.currentPiece.x = newX; player.currentPiece.x = newX;
broadcastState(room); broadcastState();
} }
}); });
socket.on('player-rotate', ({ playerId }) => { socket.on('player-rotate', ({ playerId }) => {
const roomName = socket.data.roomName; if (!lobby.gameStarted) return;
if (!roomName) return;
const room = rooms.get(roomName);
if (!room || !room.gameStarted) return;
const player = room.players.get(playerId); const player = lobby.players.get(playerId);
if (!player || player.eliminated) return; if (!player || player.eliminated) return;
const originalShape = player.currentPiece.shape; const originalShape = player.currentPiece.shape;
@@ -136,19 +124,16 @@ io.on('connection', (socket) => {
if (isValidPosition(player.currentPiece, player.currentPiece.x + kick, player.currentPiece.y, player.board, rotated)) { if (isValidPosition(player.currentPiece, player.currentPiece.x + kick, player.currentPiece.y, player.board, rotated)) {
player.currentPiece.shape = rotated; player.currentPiece.shape = rotated;
player.currentPiece.x += kick; player.currentPiece.x += kick;
broadcastState(room); broadcastState();
return; return;
} }
} }
}); });
socket.on('player-drop', ({ playerId, hard }) => { socket.on('player-drop', ({ playerId, hard }) => {
const roomName = socket.data.roomName; if (!lobby.gameStarted) return;
if (!roomName) return;
const room = rooms.get(roomName);
if (!room || !room.gameStarted) return;
const player = room.players.get(playerId); const player = lobby.players.get(playerId);
if (!player || player.eliminated) return; if (!player || player.eliminated) return;
if (hard) { if (hard) {
@@ -158,45 +143,41 @@ io.on('connection', (socket) => {
dropped++; dropped++;
} }
player.score += dropped * 2; player.score += dropped * 2;
lockPiece(room, player); lockPiece(player);
} else { } else {
const newY = player.currentPiece.y + 1; const newY = player.currentPiece.y + 1;
if (isValidPosition(player.currentPiece, player.currentPiece.x, newY, player.board)) { if (isValidPosition(player.currentPiece, player.currentPiece.x, newY, player.board)) {
player.currentPiece.y = newY; player.currentPiece.y = newY;
player.score += 1; player.score += 1;
broadcastState(room); broadcastState();
} else { } else {
lockPiece(room, player); lockPiece(player);
} }
} }
}); });
socket.on('disconnect', () => { socket.on('disconnect', () => {
const roomName = socket.data.roomName; const player = lobby.players.get(socket.id);
if (!roomName) return; if (player) {
console.log(`${player.name} disconnected (${lobby.players.size - 1} remaining)`);
const room = rooms.get(roomName); if (lobby.gameStarted) {
if (room) { player.eliminated = true;
const player = room.players.get(socket.id); broadcastState();
if (player) { checkGameOver();
console.log(`${player.name} left room ${roomName}`); }
if (room.gameStarted) { lobby.players.delete(socket.id);
player.eliminated = true;
broadcastState(room);
checkGameOver(room);
}
room.players.delete(socket.id); io.emit('player-left', {
io.to(roomName).emit('player-left', { playerId: socket.id,
playerId: socket.id, players: getPlayersList()
players: getPlayersList(room) });
});
if (room.players.size === 0) { // If lobby empty and game running, stop game
if (room.gameInterval) clearInterval(room.gameInterval); if (lobby.players.size === 0 && lobby.gameStarted) {
rooms.delete(roomName); if (lobby.gameInterval) clearInterval(lobby.gameInterval);
} lobby.gameStarted = false;
} }
} }
}); });
@@ -206,8 +187,8 @@ function createEmptyBoard() {
return Array(BOARD_HEIGHT).fill(null).map(() => Array(BOARD_WIDTH).fill(0)); return Array(BOARD_HEIGHT).fill(null).map(() => Array(BOARD_WIDTH).fill(0));
} }
function getPlayersList(room) { function getPlayersList() {
return Array.from(room.players.values()).map(p => ({ return Array.from(lobby.players.values()).map(p => ({
id: p.id, id: p.id,
name: p.name, name: p.name,
score: p.score, score: p.score,
@@ -251,7 +232,7 @@ function spawnPiece(player) {
return isValidPosition(player.currentPiece, player.currentPiece.x, player.currentPiece.y, player.board); return isValidPosition(player.currentPiece, player.currentPiece.x, player.currentPiece.y, player.board);
} }
function lockPiece(room, player) { function lockPiece(player) {
if (!player.currentPiece) return; if (!player.currentPiece) return;
for (let row = 0; row < player.currentPiece.shape.length; row++) { for (let row = 0; row < player.currentPiece.shape.length; row++) {
@@ -272,10 +253,10 @@ function lockPiece(room, player) {
if (!spawnPiece(player)) player.eliminated = true; if (!spawnPiece(player)) player.eliminated = true;
if (rowsCleared > 0) sendGarbage(room, player, rowsCleared); if (rowsCleared > 0) sendGarbage(player, rowsCleared);
broadcastState(room); broadcastState();
checkGameOver(room); checkGameOver();
} }
function clearRows(player) { function clearRows(player) {
@@ -298,9 +279,9 @@ function clearRows(player) {
return rowsCleared; return rowsCleared;
} }
function sendGarbage(room, sender, rowsCleared) { function sendGarbage(sender, rowsCleared) {
const garbageRows = rowsCleared >= 4 ? rowsCleared : Math.max(1, rowsCleared - 1); 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); const opponents = Array.from(lobby.players.values()).filter(p => p.id !== sender.id && !p.eliminated);
if (opponents.length === 0) return; if (opponents.length === 0) return;
for (let i = 0; i < garbageRows; i++) { for (let i = 0; i < garbageRows; i++) {
const target = opponents[Math.floor(Math.random() * opponents.length)]; const target = opponents[Math.floor(Math.random() * opponents.length)];
@@ -324,10 +305,10 @@ function addGarbageToPlayer(player) {
} }
} }
function startGame(room) { function startGame() {
room.gameStarted = true; lobby.gameStarted = true;
for (const player of room.players.values()) { for (const player of lobby.players.values()) {
player.board = createEmptyBoard(); player.board = createEmptyBoard();
player.score = 0; player.score = 0;
player.lines = 0; player.lines = 0;
@@ -338,18 +319,18 @@ function startGame(room) {
player.nextPiece = getRandomPiece(); player.nextPiece = getRandomPiece();
} }
room.gameInterval = setInterval(() => gameTick(room), 50); lobby.gameInterval = setInterval(() => gameTick(), 50);
io.to(room.name).emit('game-started', { io.emit('game-started', {
players: getPlayersList(room), players: getPlayersList(),
states: getStates(room) states: getStates()
}); });
console.log(`Game started in room ${room.name} with ${room.players.size} players`); console.log(`Game started with ${lobby.players.size} players`);
} }
function gameTick(room) { function gameTick() {
for (const player of room.players.values()) { for (const player of lobby.players.values()) {
if (player.eliminated) continue; if (player.eliminated) continue;
player.dropCounter += 50; player.dropCounter += 50;
@@ -358,20 +339,20 @@ function gameTick(room) {
if (isValidPosition(player.currentPiece, player.currentPiece.x, player.currentPiece.y + 1, player.board)) { if (isValidPosition(player.currentPiece, player.currentPiece.x, player.currentPiece.y + 1, player.board)) {
player.currentPiece.y++; player.currentPiece.y++;
} else { } else {
lockPiece(room, player); lockPiece(player);
} }
} }
} }
broadcastState(room); broadcastState();
checkGameOver(room); checkGameOver();
} }
function broadcastState(room) { function broadcastState() {
io.to(room.name).emit('state-update', getStates(room)); io.emit('state-update', getStates());
} }
function getStates(room) { function getStates() {
return Array.from(room.players.values()).map(p => ({ return Array.from(lobby.players.values()).map(p => ({
playerId: p.id, playerId: p.id,
board: JSON.parse(JSON.stringify(p.board)), board: JSON.parse(JSON.stringify(p.board)),
currentPiece: p.currentPiece ? JSON.parse(JSON.stringify(p.currentPiece)) : null, currentPiece: p.currentPiece ? JSON.parse(JSON.stringify(p.currentPiece)) : null,
@@ -383,15 +364,15 @@ function getStates(room) {
})); }));
} }
function checkGameOver(room) { function checkGameOver() {
const activePlayers = Array.from(room.players.values()).filter(p => !p.eliminated); const activePlayers = Array.from(lobby.players.values()).filter(p => !p.eliminated);
if (activePlayers.length <= 1) { if (activePlayers.length <= 1) {
io.to(room.name).emit('game-over', { states: getStates(room) }); io.emit('game-over', { states: getStates() });
if (room.gameInterval) { if (lobby.gameInterval) {
clearInterval(room.gameInterval); clearInterval(lobby.gameInterval);
room.gameInterval = null; lobby.gameInterval = null;
} }
room.gameStarted = false; lobby.gameStarted = false;
} }
} }