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:
@@ -1,184 +1,121 @@
|
||||
# Tetris Battle Royale - Implementation Plan
|
||||
# Single Global Lobby - 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
|
||||
```
|
||||
Changed from room-based architecture to a single global lobby where:
|
||||
- No room name input needed
|
||||
- All players join the same lobby
|
||||
- Player list shows everyone in the lobby
|
||||
- Game starts when all players are ready (minimum 2 players)
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
- [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
|
||||
- [x] Replace `rooms` Map with single `lobby` object
|
||||
- [x] Change `join-room` event to `join-lobby`
|
||||
- Remove roomName parameter
|
||||
- Only accept playerName
|
||||
- Add player to global lobby
|
||||
- [x] Update `ready` event
|
||||
- Check if all players in global lobby are ready
|
||||
- Start game when all ready (min 2 players, max 8)
|
||||
- [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
|
||||
- [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)
|
||||
- [x] Remove "Room Name" input field from login screen
|
||||
- [x] Remove room name label
|
||||
- [x] Change button text from "JOIN ROOM" to "JOIN LOBBY"
|
||||
- [x] Update lobby screen header
|
||||
- Remove room name display
|
||||
- Show "Global Lobby" instead
|
||||
|
||||
**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
|
||||
- [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
|
||||
- [x] Rename `joinRoom` to `joinLobby`
|
||||
- Remove roomName parameter
|
||||
- Emit `join-lobby` event with just playerName
|
||||
- [x] Rename `leaveRoom` to `leaveLobby`
|
||||
- Remove room name handling
|
||||
- [x] Remove `currentRoom` property (no longer needed)
|
||||
- [x] Update all event listeners to work without room names
|
||||
|
||||
**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
|
||||
- [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
|
||||
- [x] Update `handleJoin` method
|
||||
- Remove roomName input validation
|
||||
- Only validate playerName
|
||||
- Call `network.joinLobby(playerName)`
|
||||
- [x] Update lobby display
|
||||
- Show "Global Lobby" instead of room name
|
||||
- Update player list display
|
||||
- [x] Update game over "Back to Lobby" flow
|
||||
|
||||
**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
|
||||
- [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
|
||||
- [x] Update network listener setup
|
||||
- Adapt to new event structure (no room names)
|
||||
- [x] Update game start handling
|
||||
- Works with global lobby players
|
||||
- [x] Update state sync for single lobby
|
||||
|
||||
**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
|
||||
|
||||
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
|
||||
1. Start server: `cd server && node index.js`
|
||||
2. Open `http://localhost:3000` in 2+ browser tabs
|
||||
3. Enter only your name (no room name field exists)
|
||||
4. Click "JOIN LOBBY"
|
||||
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
@@ -11,23 +11,19 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- Room Selection Screen -->
|
||||
<!-- Login Screen -->
|
||||
<div id="room-screen" class="screen active">
|
||||
<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">
|
||||
<label for="player-name">Your Name</label>
|
||||
<input type="text" id="player-name" placeholder="Enter your name">
|
||||
</div>
|
||||
<button id="join-btn">JOIN ROOM</button>
|
||||
<button id="join-btn">JOIN LOBBY</button>
|
||||
</div>
|
||||
|
||||
<!-- Lobby 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>
|
||||
<button id="ready-btn">READY</button>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ function setupNetworkListeners() {
|
||||
// Game started
|
||||
network.setListener('game-started', (players, states) => {
|
||||
ui.showScreen('game');
|
||||
ui.displays.gameRoomName.textContent = network.currentRoom;
|
||||
ui.displays.gameRoomName.textContent = 'GLOBAL LOBBY';
|
||||
|
||||
// Clear old boards
|
||||
renderer.clearAll();
|
||||
|
||||
+3
-12
@@ -3,7 +3,6 @@
|
||||
class NetworkManager {
|
||||
constructor() {
|
||||
this.socket = null;
|
||||
this.currentRoom = null;
|
||||
this.currentPlayerId = null;
|
||||
this.players = {};
|
||||
this.gameState = {};
|
||||
@@ -26,7 +25,6 @@ class NetworkManager {
|
||||
|
||||
this.socket.on('disconnect', () => {
|
||||
console.log('Disconnected from server');
|
||||
this.currentRoom = null;
|
||||
});
|
||||
|
||||
this.socket.on('player-joined', ({ player, players }) => {
|
||||
@@ -66,20 +64,13 @@ class NetworkManager {
|
||||
});
|
||||
}
|
||||
|
||||
joinRoom(roomName, playerName) {
|
||||
joinLobby(playerName) {
|
||||
if (!this.socket) return;
|
||||
|
||||
this.currentRoom = roomName;
|
||||
this.socket.emit('join-room', { roomName, playerName });
|
||||
this.socket.emit('join-lobby', { playerName });
|
||||
}
|
||||
|
||||
leaveRoom() {
|
||||
leaveLobby() {
|
||||
if (!this.socket) return;
|
||||
|
||||
if (this.currentRoom) {
|
||||
this.socket.leave(this.currentRoom);
|
||||
}
|
||||
this.currentRoom = null;
|
||||
}
|
||||
|
||||
ready() {
|
||||
|
||||
+5
-12
@@ -10,7 +10,6 @@ class UIManager {
|
||||
};
|
||||
|
||||
this.inputs = {
|
||||
roomName: document.getElementById('room-name'),
|
||||
playerName: document.getElementById('player-name')
|
||||
};
|
||||
|
||||
@@ -22,7 +21,6 @@ class UIManager {
|
||||
};
|
||||
|
||||
this.displays = {
|
||||
lobbyRoomName: document.getElementById('lobby-room-name'),
|
||||
gameRoomName: document.getElementById('game-room-name'),
|
||||
playerList: document.getElementById('player-list'),
|
||||
battleGrid: document.getElementById('battle-grid'),
|
||||
@@ -41,9 +39,6 @@ class UIManager {
|
||||
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();
|
||||
});
|
||||
@@ -55,21 +50,19 @@ class UIManager {
|
||||
}
|
||||
|
||||
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');
|
||||
if (!playerName) {
|
||||
this.showMessage('Please enter your name');
|
||||
return;
|
||||
}
|
||||
|
||||
// Emit join event
|
||||
// Set listener for player joined event
|
||||
network.setListener('player-joined', () => {
|
||||
this.showScreen('lobby');
|
||||
this.displays.lobbyRoomName.textContent = roomName;
|
||||
});
|
||||
|
||||
network.joinRoom(roomName, playerName);
|
||||
network.joinLobby(playerName);
|
||||
}
|
||||
|
||||
handleReady() {
|
||||
@@ -79,7 +72,7 @@ class UIManager {
|
||||
}
|
||||
|
||||
handleLeave() {
|
||||
network.leaveRoom();
|
||||
network.leaveLobby();
|
||||
this.showScreen('room');
|
||||
}
|
||||
|
||||
|
||||
+78
-97
@@ -10,8 +10,12 @@ const io = new Server(server);
|
||||
// Serve static files
|
||||
app.use(express.static(path.join(__dirname, '../public')));
|
||||
|
||||
// Game state storage
|
||||
const rooms = new Map();
|
||||
// Single global lobby
|
||||
const lobby = {
|
||||
players: new Map(),
|
||||
gameStarted: false,
|
||||
gameInterval: null
|
||||
};
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
@@ -34,19 +38,8 @@ 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;
|
||||
// Join global lobby
|
||||
socket.on('join-lobby', ({ playerName }) => {
|
||||
socket.data.playerName = playerName;
|
||||
|
||||
const player = {
|
||||
@@ -64,60 +57,55 @@ io.on('connection', (socket) => {
|
||||
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 },
|
||||
players: getPlayersList(room)
|
||||
players: getPlayersList()
|
||||
});
|
||||
|
||||
console.log(`${playerName} joined room ${roomName}`);
|
||||
console.log(`${playerName} joined global lobby (${lobby.players.size} players)`);
|
||||
});
|
||||
|
||||
socket.on('ready', () => {
|
||||
const roomName = socket.data.roomName;
|
||||
if (!roomName) return;
|
||||
const player = lobby.players.get(socket.id);
|
||||
if (!player) return;
|
||||
|
||||
const room = rooms.get(roomName);
|
||||
const player = room.players.get(socket.id);
|
||||
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 },
|
||||
players: getPlayersList(room)
|
||||
players: getPlayersList()
|
||||
});
|
||||
|
||||
if (room.players.size >= 2 && room.players.size <= 8) {
|
||||
const allReady = Array.from(room.players.values()).every(p => p.ready);
|
||||
// Check if all players ready and min 2 players
|
||||
if (lobby.players.size >= 2 && lobby.players.size <= 8) {
|
||||
const allReady = Array.from(lobby.players.values()).every(p => p.ready);
|
||||
if (allReady) {
|
||||
startGame(room);
|
||||
startGame();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('player-move', ({ playerId, direction }) => {
|
||||
const roomName = socket.data.roomName;
|
||||
if (!roomName) return;
|
||||
const room = rooms.get(roomName);
|
||||
if (!room || !room.gameStarted) return;
|
||||
if (!lobby.gameStarted) return;
|
||||
|
||||
const player = room.players.get(playerId);
|
||||
const player = lobby.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);
|
||||
broadcastState();
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('player-rotate', ({ playerId }) => {
|
||||
const roomName = socket.data.roomName;
|
||||
if (!roomName) return;
|
||||
const room = rooms.get(roomName);
|
||||
if (!room || !room.gameStarted) return;
|
||||
if (!lobby.gameStarted) return;
|
||||
|
||||
const player = room.players.get(playerId);
|
||||
const player = lobby.players.get(playerId);
|
||||
if (!player || player.eliminated) return;
|
||||
|
||||
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)) {
|
||||
player.currentPiece.shape = rotated;
|
||||
player.currentPiece.x += kick;
|
||||
broadcastState(room);
|
||||
broadcastState();
|
||||
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;
|
||||
if (!lobby.gameStarted) return;
|
||||
|
||||
const player = room.players.get(playerId);
|
||||
const player = lobby.players.get(playerId);
|
||||
if (!player || player.eliminated) return;
|
||||
|
||||
if (hard) {
|
||||
@@ -158,45 +143,41 @@ io.on('connection', (socket) => {
|
||||
dropped++;
|
||||
}
|
||||
player.score += dropped * 2;
|
||||
lockPiece(room, player);
|
||||
lockPiece(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);
|
||||
broadcastState();
|
||||
} else {
|
||||
lockPiece(room, player);
|
||||
lockPiece(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);
|
||||
const player = lobby.players.get(socket.id);
|
||||
if (player) {
|
||||
console.log(`${player.name} left room ${roomName}`);
|
||||
console.log(`${player.name} disconnected (${lobby.players.size - 1} remaining)`);
|
||||
|
||||
if (room.gameStarted) {
|
||||
if (lobby.gameStarted) {
|
||||
player.eliminated = true;
|
||||
broadcastState(room);
|
||||
checkGameOver(room);
|
||||
broadcastState();
|
||||
checkGameOver();
|
||||
}
|
||||
|
||||
room.players.delete(socket.id);
|
||||
io.to(roomName).emit('player-left', {
|
||||
lobby.players.delete(socket.id);
|
||||
|
||||
io.emit('player-left', {
|
||||
playerId: socket.id,
|
||||
players: getPlayersList(room)
|
||||
players: getPlayersList()
|
||||
});
|
||||
|
||||
if (room.players.size === 0) {
|
||||
if (room.gameInterval) clearInterval(room.gameInterval);
|
||||
rooms.delete(roomName);
|
||||
}
|
||||
// If lobby empty and game running, stop game
|
||||
if (lobby.players.size === 0 && lobby.gameStarted) {
|
||||
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));
|
||||
}
|
||||
|
||||
function getPlayersList(room) {
|
||||
return Array.from(room.players.values()).map(p => ({
|
||||
function getPlayersList() {
|
||||
return Array.from(lobby.players.values()).map(p => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
score: p.score,
|
||||
@@ -251,7 +232,7 @@ function spawnPiece(player) {
|
||||
return isValidPosition(player.currentPiece, player.currentPiece.x, player.currentPiece.y, player.board);
|
||||
}
|
||||
|
||||
function lockPiece(room, player) {
|
||||
function lockPiece(player) {
|
||||
if (!player.currentPiece) return;
|
||||
|
||||
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 (rowsCleared > 0) sendGarbage(room, player, rowsCleared);
|
||||
if (rowsCleared > 0) sendGarbage(player, rowsCleared);
|
||||
|
||||
broadcastState(room);
|
||||
checkGameOver(room);
|
||||
broadcastState();
|
||||
checkGameOver();
|
||||
}
|
||||
|
||||
function clearRows(player) {
|
||||
@@ -298,9 +279,9 @@ function clearRows(player) {
|
||||
return rowsCleared;
|
||||
}
|
||||
|
||||
function sendGarbage(room, sender, rowsCleared) {
|
||||
function sendGarbage(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);
|
||||
const opponents = Array.from(lobby.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)];
|
||||
@@ -324,10 +305,10 @@ function addGarbageToPlayer(player) {
|
||||
}
|
||||
}
|
||||
|
||||
function startGame(room) {
|
||||
room.gameStarted = true;
|
||||
function startGame() {
|
||||
lobby.gameStarted = true;
|
||||
|
||||
for (const player of room.players.values()) {
|
||||
for (const player of lobby.players.values()) {
|
||||
player.board = createEmptyBoard();
|
||||
player.score = 0;
|
||||
player.lines = 0;
|
||||
@@ -338,18 +319,18 @@ function startGame(room) {
|
||||
player.nextPiece = getRandomPiece();
|
||||
}
|
||||
|
||||
room.gameInterval = setInterval(() => gameTick(room), 50);
|
||||
lobby.gameInterval = setInterval(() => gameTick(), 50);
|
||||
|
||||
io.to(room.name).emit('game-started', {
|
||||
players: getPlayersList(room),
|
||||
states: getStates(room)
|
||||
io.emit('game-started', {
|
||||
players: getPlayersList(),
|
||||
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) {
|
||||
for (const player of room.players.values()) {
|
||||
function gameTick() {
|
||||
for (const player of lobby.players.values()) {
|
||||
if (player.eliminated) continue;
|
||||
|
||||
player.dropCounter += 50;
|
||||
@@ -358,20 +339,20 @@ function gameTick(room) {
|
||||
if (isValidPosition(player.currentPiece, player.currentPiece.x, player.currentPiece.y + 1, player.board)) {
|
||||
player.currentPiece.y++;
|
||||
} else {
|
||||
lockPiece(room, player);
|
||||
lockPiece(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
broadcastState(room);
|
||||
checkGameOver(room);
|
||||
broadcastState();
|
||||
checkGameOver();
|
||||
}
|
||||
|
||||
function broadcastState(room) {
|
||||
io.to(room.name).emit('state-update', getStates(room));
|
||||
function broadcastState() {
|
||||
io.emit('state-update', getStates());
|
||||
}
|
||||
|
||||
function getStates(room) {
|
||||
return Array.from(room.players.values()).map(p => ({
|
||||
function getStates() {
|
||||
return Array.from(lobby.players.values()).map(p => ({
|
||||
playerId: p.id,
|
||||
board: JSON.parse(JSON.stringify(p.board)),
|
||||
currentPiece: p.currentPiece ? JSON.parse(JSON.stringify(p.currentPiece)) : null,
|
||||
@@ -383,15 +364,15 @@ function getStates(room) {
|
||||
}));
|
||||
}
|
||||
|
||||
function checkGameOver(room) {
|
||||
const activePlayers = Array.from(room.players.values()).filter(p => !p.eliminated);
|
||||
function checkGameOver() {
|
||||
const activePlayers = Array.from(lobby.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;
|
||||
io.emit('game-over', { states: getStates() });
|
||||
if (lobby.gameInterval) {
|
||||
clearInterval(lobby.gameInterval);
|
||||
lobby.gameInterval = null;
|
||||
}
|
||||
room.gameStarted = false;
|
||||
lobby.gameStarted = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user