316 lines
11 KiB
JavaScript
316 lines
11 KiB
JavaScript
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();
|
|
}
|
|
}); |