thomashamburg eb1f14d19c tetris v1
2025-09-29 17:46:48 +02:00

501 lines
18 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tetris</title>
<style>
body {
margin: 0;
padding: 20px;
background: #000;
color: #fff;
font-family: 'Courier New', monospace;
display: flex;
justify-content: center;
align-items: flex-start;
min-height: 100vh;
}
.game-container {
display: flex;
gap: 20px;
align-items: flex-start;
}
.main-game {
display: flex;
flex-direction: column;
align-items: center;
}
canvas {
border: 2px solid #333;
background: #111;
}
.info-panel {
width: 200px;
background: #222;
padding: 15px;
border-radius: 8px;
}
.info-section {
margin-bottom: 20px;
}
.info-section h3 {
margin: 0 0 10px 0;
color: #ccc;
font-size: 14px;
}
.next-canvas, .hold-canvas {
border: 1px solid #444;
background: #111;
}
.controls {
margin-top: 15px;
text-align: center;
}
.controls h3 {
margin-bottom: 10px;
}
.controls p {
margin: 5px 0;
font-size: 12px;
color: #aaa;
}
button {
background: #333;
color: #fff;
border: 1px solid #555;
padding: 8px 16px;
margin: 5px;
cursor: pointer;
border-radius: 4px;
}
button:hover {
background: #555;
}
.game-over {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.9);
padding: 30px;
border-radius: 10px;
text-align: center;
border: 2px solid #f00;
}
.score-info {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
font-size: 12px;
}
.score-info div {
text-align: center;
}
.status {
margin-top: 10px;
padding: 10px;
background: #333;
border-radius: 4px;
font-size: 12px;
}
.connection-status {
color: #0f0;
}
.connection-status.disconnected {
color: #f00;
}
</style>
</head>
<body>
<div class="game-container">
<div class="info-panel">
<div class="info-section">
<h3>Hold</h3>
<canvas id="holdCanvas" class="hold-canvas" width="80" height="80"></canvas>
</div>
<div class="info-section">
<h3>Next</h3>
<canvas id="nextCanvas" class="next-canvas" width="80" height="320"></canvas>
</div>
<div class="info-section">
<div class="score-info">
<div>Score<br><span id="score">0</span></div>
<div>Level<br><span id="level">1</span></div>
<div>Lines<br><span id="lines">0</span></div>
<div>Goal<br><span id="goal">5</span></div>
</div>
</div>
<div class="status">
<div>Status: <span id="connectionStatus" class="connection-status">Connecting...</span></div>
<div>Game: <span id="gameStatus">Not Started</span></div>
</div>
</div>
<div class="main-game">
<canvas id="gameCanvas" width="300" height="600"></canvas>
<div class="controls">
<button onclick="startGame()">Start Game</button>
<button onclick="location.reload()">Refresh</button>
<h3>Controls</h3>
<p>← → Move</p>
<p>↓ Soft Drop</p>
<p>Space Hard Drop</p>
<p>Z Rotate Left</p>
<p>X Rotate Right</p>
<p>C Hold</p>
</div>
</div>
</div>
<div id="gameOver" class="game-over" style="display: none;">
<h2>Game Over</h2>
<p>Final Score: <span id="finalScore">0</span></p>
<p>Level Reached: <span id="finalLevel">1</span></p>
<p>Lines Cleared: <span id="finalLines">0</span></p>
<button onclick="startGame()">Play Again</button>
</div>
<script>
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();
}
});
</script>
</body>
</html>