// 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.holdPiece = null; this.canHold = true; 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.holdPiece = null; this.canHold = true; 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; } hold() { if (this.gameOver || this.inputLocked || !this.canHold) return false; if (this.holdPiece === null) { // First hold - store current piece and spawn next this.holdPiece = { type: this.currentPiece.type, shape: JSON.parse(JSON.stringify(this.currentPiece.shape)), color: this.currentPiece.color }; this.currentPiece = this.nextPiece; this.nextPiece = this.getRandomPiece(); } else { // Swap with held piece const temp = { type: this.currentPiece.type, shape: JSON.parse(JSON.stringify(this.currentPiece.shape)), color: this.currentPiece.color }; this.currentPiece = { ...this.holdPiece, shape: JSON.parse(JSON.stringify(this.holdPiece.shape)), x: Math.floor(BOARD_WIDTH / 2) - Math.floor(this.holdPiece.shape[0].length / 2), y: 0 }; this.holdPiece = temp; } this.canHold = false; return true; } 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; } // Reset canHold for the new piece this.canHold = 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); } getGhostY() { if (!this.currentPiece) return null; let ghostY = this.currentPiece.y; while (this.isValidPosition(this.currentPiece.x, ghostY + 1, this.currentPiece.shape)) { ghostY++; } return ghostY; } 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() { const ghostY = this.currentPiece ? this.getGhostY() : null; return { playerId: this.playerId, board: JSON.parse(JSON.stringify(this.board)), currentPiece: this.currentPiece ? JSON.parse(JSON.stringify(this.currentPiece)) : null, ghostY: ghostY, nextPiece: this.nextPiece ? JSON.parse(JSON.stringify(this.nextPiece)) : null, holdPiece: this.holdPiece ? JSON.parse(JSON.stringify(this.holdPiece)) : null, canHold: this.canHold, 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.holdPiece = state.holdPiece ? JSON.parse(JSON.stringify(state.holdPiece)) : null; this.canHold = state.canHold !== undefined ? state.canHold : true; this.score = state.score; this.lines = state.lines; this.level = state.level; this.gameOver = state.gameOver; this.eliminated = state.eliminated; this.sequenceIndex = state.sequenceIndex || 0; 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;