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,264 @@
|
||||
// Canvas Renderer for Tetris
|
||||
|
||||
class TetrisRenderer {
|
||||
constructor(containerId) {
|
||||
this.container = document.getElementById(containerId);
|
||||
this.boards = new Map();
|
||||
}
|
||||
|
||||
createPlayerBoard(playerId, playerName) {
|
||||
const boardDiv = document.createElement('div');
|
||||
boardDiv.className = 'player-board';
|
||||
boardDiv.id = `board-${playerId}`;
|
||||
boardDiv.dataset.playerId = playerId;
|
||||
|
||||
// Main game canvas
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.id = `canvas-${playerId}`;
|
||||
canvas.width = BOARD_WIDTH * CELL_SIZE;
|
||||
canvas.height = BOARD_HEIGHT * CELL_SIZE;
|
||||
canvas.style.width = `${BOARD_WIDTH * CELL_SIZE}px`;
|
||||
canvas.style.height = `${BOARD_HEIGHT * CELL_SIZE}px`;
|
||||
boardDiv.appendChild(canvas);
|
||||
|
||||
// Player info
|
||||
const infoDiv = document.createElement('div');
|
||||
infoDiv.className = 'board-info';
|
||||
infoDiv.innerHTML = `
|
||||
<span class="name">${playerName}</span>
|
||||
<span class="stats">Lines: <span class="lines">0</span></span>
|
||||
`;
|
||||
boardDiv.appendChild(infoDiv);
|
||||
|
||||
// Next piece preview
|
||||
const nextDiv = document.createElement('div');
|
||||
nextDiv.className = 'board-info';
|
||||
const nextCanvas = document.createElement('canvas');
|
||||
nextCanvas.id = `next-${playerId}`;
|
||||
nextCanvas.width = 80;
|
||||
nextCanvas.height = 80;
|
||||
nextCanvas.style.width = '80px';
|
||||
nextCanvas.style.height = '80px';
|
||||
nextDiv.innerHTML = '<span>NEXT:</span>';
|
||||
nextDiv.appendChild(nextCanvas);
|
||||
boardDiv.appendChild(nextDiv);
|
||||
|
||||
this.container.appendChild(boardDiv);
|
||||
|
||||
this.boards.set(playerId, {
|
||||
element: boardDiv,
|
||||
canvas: canvas,
|
||||
nextCanvas: nextCanvas,
|
||||
info: infoDiv,
|
||||
ctx: canvas.getContext('2d'),
|
||||
nextCtx: nextCanvas.getContext('2d')
|
||||
});
|
||||
|
||||
return boardDiv;
|
||||
}
|
||||
|
||||
removePlayerBoard(playerId) {
|
||||
const board = this.boards.get(playerId);
|
||||
if (board) {
|
||||
board.element.remove();
|
||||
this.boards.delete(playerId);
|
||||
}
|
||||
}
|
||||
|
||||
renderPlayer(playerId, gameState) {
|
||||
const boardData = this.boards.get(playerId);
|
||||
if (!boardData) return;
|
||||
|
||||
const { ctx, nextCtx, element, info } = boardData;
|
||||
|
||||
// Clear canvas
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
|
||||
// Draw grid lines
|
||||
this.drawGrid(ctx);
|
||||
|
||||
// Draw locked pieces
|
||||
if (gameState && gameState.board) {
|
||||
this.drawBoard(ctx, gameState.board);
|
||||
}
|
||||
|
||||
// Draw current piece
|
||||
if (gameState && gameState.currentPiece && !gameState.gameOver) {
|
||||
this.drawPiece(ctx, gameState.currentPiece);
|
||||
}
|
||||
|
||||
// Draw next piece
|
||||
if (gameState && gameState.nextPiece) {
|
||||
this.drawNextPiece(nextCtx, gameState.nextPiece);
|
||||
}
|
||||
|
||||
// Update stats
|
||||
if (gameState) {
|
||||
const linesSpan = info.querySelector('.lines');
|
||||
if (linesSpan) {
|
||||
linesSpan.textContent = gameState.lines;
|
||||
}
|
||||
}
|
||||
|
||||
// Update board state classes
|
||||
element.classList.remove('active', 'eliminated');
|
||||
if (gameState && gameState.eliminated) {
|
||||
element.classList.add('eliminated');
|
||||
}
|
||||
}
|
||||
|
||||
drawGrid(ctx) {
|
||||
ctx.strokeStyle = '#222';
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
for (let x = 0; x <= BOARD_WIDTH; x++) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x * CELL_SIZE, 0);
|
||||
ctx.lineTo(x * CELL_SIZE, BOARD_HEIGHT * CELL_SIZE);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
for (let y = 0; y <= BOARD_HEIGHT; y++) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y * CELL_SIZE);
|
||||
ctx.lineTo(BOARD_WIDTH * CELL_SIZE, y * CELL_SIZE);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
drawBoard(ctx, board) {
|
||||
for (let row = 0; row < BOARD_HEIGHT; row++) {
|
||||
for (let col = 0; col < BOARD_WIDTH; col++) {
|
||||
const color = board[row][col];
|
||||
if (color) {
|
||||
this.drawCell(ctx, col, row, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawPiece(ctx, piece) {
|
||||
for (let row = 0; row < piece.shape.length; row++) {
|
||||
for (let col = 0; col < piece.shape[row].length; col++) {
|
||||
if (piece.shape[row][col]) {
|
||||
const x = piece.x + col;
|
||||
const y = piece.y + row;
|
||||
if (y >= 0) {
|
||||
this.drawCell(ctx, x, y, piece.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawCell(ctx, x, y, color) {
|
||||
const px = x * CELL_SIZE;
|
||||
const py = y * CELL_SIZE;
|
||||
|
||||
// Main fill
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(px, py, CELL_SIZE, CELL_SIZE);
|
||||
|
||||
// Bevel effect
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.3)';
|
||||
ctx.fillRect(px, py, CELL_SIZE, 3);
|
||||
ctx.fillRect(px, py, 3, CELL_SIZE);
|
||||
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.3)';
|
||||
ctx.fillRect(px, py + CELL_SIZE - 3, CELL_SIZE, 3);
|
||||
ctx.fillRect(px + CELL_SIZE - 3, py, 3, CELL_SIZE);
|
||||
|
||||
// Border
|
||||
ctx.strokeStyle = 'rgba(0,0,0,0.5)';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(px, py, CELL_SIZE, CELL_SIZE);
|
||||
}
|
||||
|
||||
drawNextPiece(ctx, piece) {
|
||||
// Clear
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
|
||||
// Center the piece
|
||||
const pieceWidth = piece.shape[0].length * 20;
|
||||
const pieceHeight = piece.shape.length * 20;
|
||||
const offsetX = (ctx.canvas.width - pieceWidth) / 2;
|
||||
const offsetY = (ctx.canvas.height - pieceHeight) / 2;
|
||||
|
||||
for (let row = 0; row < piece.shape.length; row++) {
|
||||
for (let col = 0; col < piece.shape[row].length; col++) {
|
||||
if (piece.shape[row][col]) {
|
||||
const x = offsetX + col * 20;
|
||||
const y = offsetY + row * 20;
|
||||
|
||||
ctx.fillStyle = piece.color;
|
||||
ctx.fillRect(x, y, 20, 20);
|
||||
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.3)';
|
||||
ctx.fillRect(x, y, 20, 3);
|
||||
ctx.fillRect(x, y, 3, 20);
|
||||
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.3)';
|
||||
ctx.fillRect(x, y + 17, 20, 3);
|
||||
ctx.fillRect(x + 17, y, 3, 20);
|
||||
|
||||
ctx.strokeStyle = 'rgba(0,0,0,0.5)';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(x, y, 20, 20);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setActivePlayer(playerId) {
|
||||
// Remove active class from all boards
|
||||
this.boards.forEach((boardData) => {
|
||||
boardData.element.classList.remove('active');
|
||||
});
|
||||
|
||||
// Add to current player
|
||||
const boardData = this.boards.get(playerId);
|
||||
if (boardData) {
|
||||
boardData.element.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
triggerShake(playerId) {
|
||||
const boardData = this.boards.get(playerId);
|
||||
if (boardData) {
|
||||
boardData.element.classList.remove('shake');
|
||||
void boardData.element.offsetWidth; // Trigger reflow
|
||||
boardData.element.classList.add('shake');
|
||||
}
|
||||
}
|
||||
|
||||
triggerFlash(playerId) {
|
||||
const boardData = this.boards.get(playerId);
|
||||
if (boardData) {
|
||||
boardData.element.classList.remove('flash');
|
||||
void boardData.element.offsetWidth; // Trigger reflow
|
||||
boardData.element.classList.add('flash');
|
||||
}
|
||||
}
|
||||
|
||||
updatePlayerInfo(playerId, name, score, lines) {
|
||||
const boardData = this.boards.get(playerId);
|
||||
if (boardData) {
|
||||
const nameSpan = boardData.info.querySelector('.name');
|
||||
if (nameSpan) nameSpan.textContent = name;
|
||||
|
||||
const linesSpan = boardData.info.querySelector('.lines');
|
||||
if (linesSpan) linesSpan.textContent = lines;
|
||||
}
|
||||
}
|
||||
|
||||
clearAll() {
|
||||
this.boards.forEach((boardData) => {
|
||||
boardData.element.remove();
|
||||
});
|
||||
this.boards.clear();
|
||||
}
|
||||
}
|
||||
|
||||
window.TetrisRenderer = TetrisRenderer;
|
||||
Reference in New Issue
Block a user