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
This commit is contained in:
@@ -0,0 +1,324 @@
|
||||
// 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;
|
||||
Reference in New Issue
Block a user