Add performance improvements documentation and game-logic enhancements

- Add IMPROVEMENTS.md with detailed analysis of performance issues and bugs
- Update CLAUDE.md with negative Y overflow explanation
- Update README.md with socket events documentation
- Enhance game-logic.js with improved comments and validation
- Improve server/index.js with better documentation and edge case handling
This commit is contained in:
2026-03-24 20:46:34 +00:00
parent 1af068923b
commit aeacf9e68f
5 changed files with 335 additions and 5 deletions
+37 -2
View File
@@ -49,6 +49,9 @@ function getPieceFromType(pieceType) {
* @param {number[][]} board - The game board
* @param {number[][]} shape - Optional custom shape (for rotation)
* @returns {boolean} True if position is valid
*
* Note: Negative Y is allowed (overflow/spawn area above visible board).
* This allows pieces to be pushed up by garbage without immediate elimination.
*/
function isValidPosition(piece, x, y, board, shape = null) {
const pieceShape = shape || piece.shape;
@@ -59,11 +62,14 @@ function isValidPosition(piece, x, y, board, shape = null) {
const newY = y + row;
// Check bounds
// Note: Only checks if piece goes BELOW board (newY >= 20)
// Negative Y is allowed - pieces can exist above visible board
if (newX < 0 || newX >= BOARD_WIDTH || newY >= BOARD_HEIGHT) {
return false;
}
// Check collision with locked pieces (only if on board)
// Note: Only checks collision if piece is in visible area (newY >= 0)
if (newY >= 0 && board[newY][newX]) {
return false;
}
@@ -93,6 +99,14 @@ function getGhostY(piece, board) {
* Check if board has overflowed (blocks in top 2 rows)
* @param {number[][]} board - The game board
* @returns {boolean} True if board has overflowed
*
* Checks rows 0 AND 1 because:
* - I piece at y=0 only places blocks in row 1 (unique shape)
* - T piece at y=0 also only fills row 1
* - If we only checked row 0, these pieces would NOT trigger elimination
*
* This is called when a piece LOCKS, not when garbage is received.
* Player is eliminated only when they lock a piece that overflows.
*/
function checkBoardOverflow(board) {
for (let row = 0; row < 2; row++) {
@@ -110,18 +124,30 @@ function checkBoardOverflow(board) {
* @param {number[][]} board - The game board
* @param {object} currentPiece - Current falling piece (may be null)
* @returns {object} Object with new board, updated piece, and gap position
*
* When garbage is added:
* - All board rows shift up by 1 (top row removed)
* - New garbage row added at bottom with one random gap
* - Current piece y position decreases by 1 (pushed up)
*
* Note: Piece y can become negative (above visible board). This is intentional
* - Player is NOT eliminated when piece goes negative
* - Piece will drop naturally back into visible area
* - Elimination only occurs when piece LOCKS with blocks in rows 0-1
*/
function addGarbageRow(board, currentPiece) {
// Remove top row
// Remove top row (all rows shift up)
const newBoard = board.slice(1);
// Add garbage row with random gap
// Add garbage row with random gap at bottom
const garbageRow = Array(BOARD_WIDTH).fill(GARBAGE_COLOR);
const gap = Math.floor(Math.random() * BOARD_WIDTH);
garbageRow[gap] = 0;
newBoard.push(garbageRow);
// Push current piece up by 1 row
// Note: y can become negative (above visible board) - this is intentional
// Player survives and piece will drop naturally back into view
let updatedPiece = currentPiece;
if (currentPiece) {
updatedPiece = {
@@ -138,6 +164,13 @@ function addGarbageRow(board, currentPiece) {
* @param {object} piece - The piece to lock
* @param {number[][]} board - The game board
* @returns {number[][]} New board with piece locked
*
* Only locks blocks that are within visible board bounds [0, 20).
* Blocks at negative Y (above board) are clipped and not locked.
*
* Example: I piece at y=-2 has blocks at rows -2,-1,0,1
* - Only blocks at rows 0 and 1 get locked to board
* - Blocks at rows -2 and -1 are clipped (discarded)
*/
function lockPieceToBoard(piece, board) {
const newBoard = board.map(row => [...row]);
@@ -148,6 +181,8 @@ function lockPieceToBoard(piece, board) {
const boardY = piece.y + row;
const boardX = piece.x + col;
// Only lock blocks within visible board bounds
// Blocks above board (negative Y) are clipped
if (boardY >= 0 && boardY < BOARD_HEIGHT && boardX >= 0 && boardX < BOARD_WIDTH) {
newBoard[boardY][boardX] = piece.color;
}
+67 -3
View File
@@ -1,3 +1,12 @@
/**
* Tetris Battle Royale Server
*
* Express + Socket.io server that handles all game logic authoritatively.
* Manages a single global lobby with 2-8 players competing in real-time.
*
* @module server
*/
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
@@ -29,7 +38,42 @@ const io = new Server(server);
// Serve static files
app.use(express.static(path.join(__dirname, '../public')));
// Single global lobby
/**
* @typedef {Object} Player
* @property {string} id - Socket.io connection ID
* @property {string} name - Player's display name
* @property {number} score - Current score
* @property {number} lines - Total lines cleared
* @property {number} level - Current level (affects drop speed)
* @property {number[][]} board - 20x10 game board
* @property {object|null} currentPiece - Currently falling piece
* @property {object|null} nextPiece - Next piece to spawn
* @property {object|null} holdPiece - Held piece (for hold mechanic)
* @property {boolean} canHold - Whether hold is available this turn
* @property {boolean} eliminated - Whether player has been eliminated
* @property {boolean} ready - Whether player is ready to start
* @property {number} dropCounter - Frame counter for auto-drop
* @property {number} dropInterval - Milliseconds between auto-drops
* @property {object[]} garbageReceived - History of garbage received
*/
/**
* @typedef {Object} Spectator
* @property {string} id - Socket.io connection ID
* @property {string} name - Spectator's display name
*/
/**
* Global lobby state - holds all game state server-side
*
* @type {Object}
* @property {Map<string, Player>} players - Active players (socketId -> Player)
* @property {Map<string, Spectator>} spectators - Spectators (socketId -> Spectator)
* @property {boolean} gameStarted - Whether game is currently running
* @property {NodeJS.Timeout|null} gameInterval - setInterval reference for game tick
* @property {string[]} pieceQueue - Shared piece queue (7-bag system)
* @property {Map<string, number>} playerSequenceIndex - Each player's position in piece queue
*/
const lobby = {
players: new Map(),
spectators: new Map(),
@@ -45,7 +89,12 @@ const LOBBY_ROOM = 'global-lobby';
io.on('connection', (socket) => {
console.log('Player connected:', socket.id);
// Join global lobby
/**
* Handle player joining the lobby
* @event join-lobby
* @param {object} data - Event data
* @param {string} data.playerName - Name of the player joining
*/
socket.on('join-lobby', ({ playerName }) => {
// Add socket to lobby room so they receive lobby events
socket.join(LOBBY_ROOM);
@@ -103,6 +152,10 @@ io.on('connection', (socket) => {
console.log(`${playerName} joined global lobby (${lobby.players.size} players)`);
});
/**
* Handle player marking themselves as ready
* @event ready
*/
socket.on('ready', () => {
const player = lobby.players.get(socket.id);
if (!player) return;
@@ -124,6 +177,10 @@ io.on('connection', (socket) => {
}
});
/**
* Handle player marking themselves as not ready
* @event unready
*/
socket.on('unready', () => {
const player = lobby.players.get(socket.id);
if (!player) return;
@@ -137,6 +194,13 @@ io.on('connection', (socket) => {
});
});
/**
* Handle player moving piece left or right
* @event player-move
* @param {object} data - Event data
* @param {string} data.playerId - ID of the player
* @param {'left'|'right'} data.direction - Direction to move
*/
socket.on('player-move', ({ playerId, direction }) => {
if (!lobby.gameStarted) return;
@@ -590,4 +654,4 @@ function checkGameOver() {
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!`);
});
});