Files
battle-royal-tetris/public/js/game.js
T
jozamudi 5da6033704 Initial commit: Tetris Battle Royale multiplayer game
Features:
- 2-8 player multiplayer via Socket.io WebSocket
- Real-time board synchronization - all players see all boards
- Battle royale mechanic: clearing rows sends garbage to opponents
- Classic Tetris gameplay with all 7 tetrominoes
- Retro visual styling with CRT scanlines and pixel font
- Automatic level progression and speed increase
- Player elimination and winner announcement

Files:
- server/index.js: Node.js + Socket.io game server
- public/js/: Frontend game logic, rendering, network, and UI
- public/css/style.css: Retro Tetris styling
- README.md: Setup and usage instructions
- PLAN.md: Implementation plan with all phases completed
2026-03-20 00:34:06 -07:00

325 lines
8.6 KiB
JavaScript

// 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.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.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;
}
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;
}
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);
}
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() {
return {
playerId: this.playerId,
board: JSON.parse(JSON.stringify(this.board)),
currentPiece: this.currentPiece ? JSON.parse(JSON.stringify(this.currentPiece)) : null,
nextPiece: this.nextPiece ? JSON.parse(JSON.stringify(this.nextPiece)) : null,
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.score = state.score;
this.lines = state.lines;
this.level = state.level;
this.gameOver = state.gameOver;
this.eliminated = state.eliminated;
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;