const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); const nextCanvas = document.getElementById('nextCanvas'); const nextCtx = nextCanvas.getContext('2d'); const holdCanvas = document.getElementById('holdCanvas'); const holdCtx = holdCanvas.getContext('2d'); const CELL_SIZE = 30; const COLORS = [ '#000000', // Empty '#00f5ff', // I - Cyan '#ffff00', // O - Yellow '#800080', // T - Purple '#00ff00', // S - Green '#ff0000', // Z - Red '#0000ff', // J - Blue '#ffa500' // L - Orange ]; let gameState = null; let eventSource = null; // Tetrimino shapes for rendering (only first rotation needed for display) const shapes = { 1: [[0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0]], // I 2: [[0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]], // O 3: [[0, 1, 0, 0], [1, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]], // T 4: [[0, 1, 1, 0], [1, 1, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], // S 5: [[1, 1, 0, 0], [0, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]], // Z 6: [[1, 0, 0, 0], [1, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]], // J 7: [[0, 0, 1, 0], [1, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]] // L }; // Current piece shapes with all rotations const currentPieceShapes = { 1: [ // I piece [[0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0]], [[0, 0, 1, 0], [0, 0, 1, 0], [0, 0, 1, 0], [0, 0, 1, 0]], [[0, 0, 0, 0], [0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0]], [[0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0]] ], 2: [ // O piece [[0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]], [[0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]], [[0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]], [[0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]] ], 3: [ // T piece [[0, 1, 0, 0], [1, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]], [[0, 1, 0, 0], [0, 1, 1, 0], [0, 1, 0, 0], [0, 0, 0, 0]], [[0, 0, 0, 0], [1, 1, 1, 0], [0, 1, 0, 0], [0, 0, 0, 0]], [[0, 1, 0, 0], [1, 1, 0, 0], [0, 1, 0, 0], [0, 0, 0, 0]] ], 4: [ // S piece [[0, 1, 1, 0], [1, 1, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], [[0, 1, 0, 0], [0, 1, 1, 0], [0, 0, 1, 0], [0, 0, 0, 0]], [[0, 0, 0, 0], [0, 1, 1, 0], [1, 1, 0, 0], [0, 0, 0, 0]], [[1, 0, 0, 0], [1, 1, 0, 0], [0, 1, 0, 0], [0, 0, 0, 0]] ], 5: [ // Z piece [[1, 1, 0, 0], [0, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]], [[0, 0, 1, 0], [0, 1, 1, 0], [0, 1, 0, 0], [0, 0, 0, 0]], [[0, 0, 0, 0], [1, 1, 0, 0], [0, 1, 1, 0], [0, 0, 0, 0]], [[0, 1, 0, 0], [1, 1, 0, 0], [1, 0, 0, 0], [0, 0, 0, 0]] ], 6: [ // J piece [[1, 0, 0, 0], [1, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]], [[0, 1, 1, 0], [0, 1, 0, 0], [0, 1, 0, 0], [0, 0, 0, 0]], [[0, 0, 0, 0], [1, 1, 1, 0], [0, 0, 1, 0], [0, 0, 0, 0]], [[0, 1, 0, 0], [0, 1, 0, 0], [1, 1, 0, 0], [0, 0, 0, 0]] ], 7: [ // L piece [[0, 0, 1, 0], [1, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]], [[0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 1, 0], [0, 0, 0, 0]], [[0, 0, 0, 0], [1, 1, 1, 0], [1, 0, 0, 0], [0, 0, 0, 0]], [[1, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0], [0, 0, 0, 0]] ] }; function connectEventSource() { if (eventSource) { eventSource.close(); } eventSource = new EventSource('/events'); eventSource.onopen = function () { updateConnectionStatus('Connected', true); }; eventSource.onmessage = function (event) { gameState = JSON.parse(event.data); render(); updateUI(); }; eventSource.onerror = function (event) { console.log('EventSource failed:', event); updateConnectionStatus('Disconnected', false); setTimeout(connectEventSource, 2000); }; } function updateConnectionStatus(status, connected) { const statusElement = document.getElementById('connectionStatus'); statusElement.textContent = status; statusElement.className = connected ? 'connection-status' : 'connection-status disconnected'; } function sendAction(action, direction = '') { const url = '/action?action=' + action + (direction ? '&direction=' + direction : ''); fetch(url, { method: 'POST' }).catch(console.error); } function startGame() { document.getElementById('gameOver').style.display = 'none'; sendAction('start'); } function render() { if (!gameState) return; // Clear main canvas ctx.fillStyle = '#111'; ctx.fillRect(0, 0, canvas.width, canvas.height); // Draw board (only visible part: rows 0-19, but our array is 0-39) for (let y = 0; y < 20; y++) { for (let x = 0; x < 10; x++) { const cellValue = gameState.board[y][x]; if (cellValue > 0) { ctx.fillStyle = COLORS[cellValue]; ctx.fillRect(x * CELL_SIZE, (19 - y) * CELL_SIZE, CELL_SIZE, CELL_SIZE); ctx.strokeStyle = '#333'; ctx.strokeRect(x * CELL_SIZE, (19 - y) * CELL_SIZE, CELL_SIZE, CELL_SIZE); } } } // Draw current piece if (gameState.current_piece && gameState.game_running) { const piece = gameState.current_piece; const shape = currentPieceShapes[piece.type][piece.rotation]; if (shape) { ctx.fillStyle = COLORS[piece.type]; for (let py = 0; py < 4; py++) { for (let px = 0; px < 4; px++) { if (shape[py][px]) { const boardX = piece.x + px; const boardY = piece.y + py; if (boardY < 20) { // Only draw visible part ctx.fillRect(boardX * CELL_SIZE, (19 - boardY) * CELL_SIZE, CELL_SIZE, CELL_SIZE); ctx.strokeStyle = '#666'; ctx.strokeRect(boardX * CELL_SIZE, (19 - boardY) * CELL_SIZE, CELL_SIZE, CELL_SIZE); } } } } } } // Draw grid ctx.strokeStyle = '#333'; ctx.lineWidth = 1; for (let x = 0; x <= 10; x++) { ctx.beginPath(); ctx.moveTo(x * CELL_SIZE, 0); ctx.lineTo(x * CELL_SIZE, canvas.height); ctx.stroke(); } for (let y = 0; y <= 20; y++) { ctx.beginPath(); ctx.moveTo(0, y * CELL_SIZE); ctx.lineTo(canvas.width, y * CELL_SIZE); ctx.stroke(); } renderNextPieces(); renderHoldPiece(); } function renderNextPieces() { nextCtx.fillStyle = '#111'; nextCtx.fillRect(0, 0, nextCanvas.width, nextCanvas.height); if (!gameState || !gameState.next_pieces) return; for (let i = 0; i < Math.min(6, gameState.next_pieces.length); i++) { const pieceType = gameState.next_pieces[i]; const shape = shapes[pieceType]; if (shape) { nextCtx.fillStyle = COLORS[pieceType]; const offsetY = i * 50 + 5; for (let py = 0; py < 4; py++) { for (let px = 0; px < 4; px++) { if (shape[py][px]) { nextCtx.fillRect(px * 15 + 10, py * 15 + offsetY, 15, 15); nextCtx.strokeStyle = '#666'; nextCtx.strokeRect(px * 15 + 10, py * 15 + offsetY, 15, 15); } } } } } } function renderHoldPiece() { holdCtx.fillStyle = '#111'; holdCtx.fillRect(0, 0, holdCanvas.width, holdCanvas.height); if (!gameState || !gameState.hold_piece) return; const pieceType = gameState.hold_piece; const shape = shapes[pieceType]; if (shape) { holdCtx.fillStyle = gameState.can_hold ? COLORS[pieceType] : '#666'; for (let py = 0; py < 4; py++) { for (let px = 0; px < 4; px++) { if (shape[py][px]) { holdCtx.fillRect(px * 15 + 10, py * 15 + 10, 15, 15); holdCtx.strokeStyle = '#444'; holdCtx.strokeRect(px * 15 + 10, py * 15 + 10, 15, 15); } } } } } function updateUI() { if (!gameState) return; document.getElementById('score').textContent = gameState.score.toLocaleString(); document.getElementById('level').textContent = gameState.level; document.getElementById('lines').textContent = gameState.lines_cleared; document.getElementById('goal').textContent = gameState.level * gameState.lines_for_level; // Update game status let gameStatus = 'Not Started'; if (gameState.game_over) { gameStatus = 'Game Over'; } else if (gameState.game_running) { gameStatus = 'Playing'; } document.getElementById('gameStatus').textContent = gameStatus; if (gameState.game_over) { document.getElementById('finalScore').textContent = gameState.score.toLocaleString(); document.getElementById('finalLevel').textContent = gameState.level; document.getElementById('finalLines').textContent = gameState.lines_cleared; document.getElementById('gameOver').style.display = 'block'; } } // Keyboard controls document.addEventListener('keydown', function (event) { if (!gameState || !gameState.game_running) return; switch (event.code) { case 'ArrowLeft': event.preventDefault(); sendAction('move', 'left'); break; case 'ArrowRight': event.preventDefault(); sendAction('move', 'right'); break; case 'ArrowDown': event.preventDefault(); sendAction('move', 'down'); break; case 'Space': event.preventDefault(); sendAction('hard_drop'); break; case 'KeyZ': event.preventDefault(); sendAction('rotate', 'ccw'); break; case 'KeyX': case 'ArrowUp': event.preventDefault(); sendAction('rotate', 'cw'); break; case 'KeyC': event.preventDefault(); sendAction('hold'); break; case 'KeyR': if (event.ctrlKey) { event.preventDefault(); location.reload(); } break; } }); // Prevent context menu on right click document.addEventListener('contextmenu', function (e) { e.preventDefault(); }); // Initialize connection when page loads window.addEventListener('load', function () { connectEventSource(); }); // Handle page visibility changes document.addEventListener('visibilitychange', function () { if (document.hidden) { if (eventSource) { eventSource.close(); } } else { connectEventSource(); } });