Files
battle-royal-tetris/server/index.js
T
jozamudi 833256d18f Fix lobby: only show players who have joined
Changes:
- Added socket.join(LOBBY_ROOM) when player joins lobby
- Changed io.emit() to io.to(LOBBY_ROOM).emit() for all lobby events
- Players only see lobby events after they click JOIN LOBBY
- Fixed player list to update correctly when players join/ready
- Game now starts properly when all players are ready

Files modified:
- server/index.js: Use Socket.io rooms for lobby scoping
- public/js/app.js: Show lobby screen on player-joined event
- public/js/ui.js: Removed duplicate listener override
2026-03-20 07:30:42 -07:00

386 lines
11 KiB
JavaScript

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')));
// Single global lobby
const lobby = {
players: new Map(),
gameStarted: false,
gameInterval: null
};
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';
const LOBBY_ROOM = 'global-lobby';
io.on('connection', (socket) => {
console.log('Player connected:', socket.id);
// Join global lobby
socket.on('join-lobby', ({ playerName }) => {
// Add socket to lobby room so they receive lobby events
socket.join(LOBBY_ROOM);
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
};
lobby.players.set(socket.id, player);
// Broadcast only to players in the lobby room
io.to(LOBBY_ROOM).emit('player-joined', {
player: { id: player.id, name: player.name },
players: getPlayersList()
});
console.log(`${playerName} joined global lobby (${lobby.players.size} players)`);
});
socket.on('ready', () => {
const player = lobby.players.get(socket.id);
if (!player) return;
player.ready = true;
// Broadcast only to players in the lobby room
io.to(LOBBY_ROOM).emit('player-joined', {
player: { id: player.id, name: player.name, ready: player.ready },
players: getPlayersList()
});
// 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();
}
}
});
socket.on('player-move', ({ playerId, direction }) => {
if (!lobby.gameStarted) return;
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();
}
});
socket.on('player-rotate', ({ playerId }) => {
if (!lobby.gameStarted) return;
const player = lobby.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();
return;
}
}
});
socket.on('player-drop', ({ playerId, hard }) => {
if (!lobby.gameStarted) return;
const player = lobby.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(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();
} else {
lockPiece(player);
}
}
});
socket.on('disconnect', () => {
const player = lobby.players.get(socket.id);
if (player) {
console.log(`${player.name} disconnected (${lobby.players.size - 1} remaining)`);
if (lobby.gameStarted) {
player.eliminated = true;
broadcastState();
checkGameOver();
}
lobby.players.delete(socket.id);
io.to(LOBBY_ROOM).emit('player-left', {
playerId: socket.id,
players: getPlayersList()
});
// If lobby empty and game running, stop game
if (lobby.players.size === 0 && lobby.gameStarted) {
if (lobby.gameInterval) clearInterval(lobby.gameInterval);
lobby.gameStarted = false;
}
}
});
});
function createEmptyBoard() {
return Array(BOARD_HEIGHT).fill(null).map(() => Array(BOARD_WIDTH).fill(0));
}
function getPlayersList() {
return Array.from(lobby.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(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(player, rowsCleared);
broadcastState();
checkGameOver();
}
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(sender, rowsCleared) {
const garbageRows = rowsCleared >= 4 ? rowsCleared : Math.max(1, rowsCleared - 1);
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)];
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() {
lobby.gameStarted = true;
for (const player of lobby.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();
}
lobby.gameInterval = setInterval(() => gameTick(), 50);
io.to(LOBBY_ROOM).emit('game-started', {
players: getPlayersList(),
states: getStates()
});
console.log(`Game started with ${lobby.players.size} players`);
}
function gameTick() {
for (const player of lobby.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(player);
}
}
}
broadcastState();
checkGameOver();
}
function broadcastState() {
io.to(LOBBY_ROOM).emit('state-update', getStates());
}
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,
nextPiece: p.nextPiece ? JSON.parse(JSON.stringify(p.nextPiece)) : null,
score: p.score,
lines: p.lines,
level: p.level,
eliminated: p.eliminated
}));
}
function checkGameOver() {
const activePlayers = Array.from(lobby.players.values()).filter(p => !p.eliminated);
if (activePlayers.length <= 1) {
io.to(LOBBY_ROOM).emit('game-over', { states: getStates() });
if (lobby.gameInterval) {
clearInterval(lobby.gameInterval);
lobby.gameInterval = null;
}
lobby.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!`);
});