feat: add Tetris game
This commit is contained in:
237
tetris.html
Normal file
237
tetris.html
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Tetris</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<style>
|
||||||
|
#wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
#sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
color: #a0a0c0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding-top: 4px;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
#sidebar .label { color: #555; font-size: 0.7rem; letter-spacing: 2px; }
|
||||||
|
#sidebar .value { color: #4ecca3; font-size: 1.1rem; font-weight: bold; }
|
||||||
|
#next-canvas { border: 1px solid #2a2a4a; display: block; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a class="menu-link" href="index.html">← Menü</a>
|
||||||
|
<h1>TETRIS</h1>
|
||||||
|
|
||||||
|
<div id="wrapper">
|
||||||
|
<canvas id="canvas" width="300" height="600"></canvas>
|
||||||
|
<div id="sidebar">
|
||||||
|
<div>
|
||||||
|
<div class="label">SCORE</div>
|
||||||
|
<div class="value" id="score-display">0</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="label">LEVEL</div>
|
||||||
|
<div class="value" id="level-display">1</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="label">LINES</div>
|
||||||
|
<div class="value" id="lines-display">0</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="label">BEST</div>
|
||||||
|
<div class="value" id="hs-display">0</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="label">NÄCHSTES</div>
|
||||||
|
<canvas id="next-canvas" width="80" height="80"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overlay" id="overlay">
|
||||||
|
<h2>GAME OVER</h2>
|
||||||
|
<p id="final-score"></p>
|
||||||
|
<button class="retro-btn" onclick="startGame()">NEU STARTEN</button>
|
||||||
|
<a class="retro-btn" href="index.html" style="text-decoration:none;text-align:center;">← MENÜ</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="scores.js"></script>
|
||||||
|
<script>
|
||||||
|
const COLS = 10, ROWS = 20, BLOCK = 30;
|
||||||
|
const COLORS = ['', '#ff6b6b','#ffd93d','#6bcb77','#4d96ff','#ff9f43','#a29bfe','#fd79a8'];
|
||||||
|
const SHAPES = [
|
||||||
|
[],
|
||||||
|
[[1,1,1,1]],
|
||||||
|
[[2,2],[2,2]],
|
||||||
|
[[0,3,0],[3,3,3]],
|
||||||
|
[[0,4,4],[4,4,0]],
|
||||||
|
[[5,5,0],[0,5,5]],
|
||||||
|
[[6,0,0],[6,6,6]],
|
||||||
|
[[0,0,7],[7,7,7]],
|
||||||
|
];
|
||||||
|
|
||||||
|
const canvas = document.getElementById('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const nextCanvas = document.getElementById('next-canvas');
|
||||||
|
const nextCtx = nextCanvas.getContext('2d');
|
||||||
|
const overlay = document.getElementById('overlay');
|
||||||
|
|
||||||
|
let board, piece, nextPiece, score, level, lines, dropInterval, lastTime, animId;
|
||||||
|
|
||||||
|
function createBoard() {
|
||||||
|
return Array.from({length: ROWS}, () => Array(COLS).fill(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomPiece() {
|
||||||
|
const id = Math.ceil(Math.random() * 7);
|
||||||
|
const shape = SHAPES[id].map(r => [...r]);
|
||||||
|
return { id, shape, x: Math.floor(COLS / 2) - Math.floor(shape[0].length / 2), y: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawBlock(context, x, y, colorId, size) {
|
||||||
|
context.fillStyle = COLORS[colorId];
|
||||||
|
context.fillRect(x * size + 1, y * size + 1, size - 2, size - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawBoard() {
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
board.forEach((row, y) => row.forEach((val, x) => { if (val) drawBlock(ctx, x, y, val, BLOCK); }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawPiece(p) {
|
||||||
|
p.shape.forEach((row, dy) => row.forEach((val, dx) => {
|
||||||
|
if (val) drawBlock(ctx, p.x + dx, p.y + dy, val, BLOCK);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawNext() {
|
||||||
|
nextCtx.clearRect(0, 0, nextCanvas.width, nextCanvas.height);
|
||||||
|
const size = 20;
|
||||||
|
const offX = Math.floor((4 - nextPiece.shape[0].length) / 2);
|
||||||
|
const offY = Math.floor((4 - nextPiece.shape.length) / 2);
|
||||||
|
nextPiece.shape.forEach((row, dy) => row.forEach((val, dx) => {
|
||||||
|
if (val) drawBlock(nextCtx, offX + dx, offY + dy, val, size);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function collides(p, board) {
|
||||||
|
return p.shape.some((row, dy) => row.some((val, dx) => {
|
||||||
|
if (!val) return false;
|
||||||
|
const nx = p.x + dx, ny = p.y + dy;
|
||||||
|
return nx < 0 || nx >= COLS || ny >= ROWS || (ny >= 0 && board[ny][nx]);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function merge(p) {
|
||||||
|
p.shape.forEach((row, dy) => row.forEach((val, dx) => {
|
||||||
|
if (val) board[p.y + dy][p.x + dx] = val;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLines() {
|
||||||
|
let cleared = 0;
|
||||||
|
for (let y = ROWS - 1; y >= 0; y--) {
|
||||||
|
if (board[y].every(v => v)) {
|
||||||
|
board.splice(y, 1);
|
||||||
|
board.unshift(Array(COLS).fill(0));
|
||||||
|
cleared++;
|
||||||
|
y++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cleared) {
|
||||||
|
const pts = [0, 100, 300, 500, 800][cleared] * level;
|
||||||
|
score += pts;
|
||||||
|
lines += cleared;
|
||||||
|
level = Math.floor(lines / 10) + 1;
|
||||||
|
dropInterval = Math.max(100, 1000 - (level - 1) * 90);
|
||||||
|
updateHUD();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotate(shape) {
|
||||||
|
return shape[0].map((_, i) => shape.map(row => row[i]).reverse());
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHUD() {
|
||||||
|
document.getElementById('score-display').textContent = score.toLocaleString('de-DE');
|
||||||
|
document.getElementById('level-display').textContent = level;
|
||||||
|
document.getElementById('lines-display').textContent = lines;
|
||||||
|
document.getElementById('hs-display').textContent = getHighscore('tetris').toLocaleString('de-DE');
|
||||||
|
}
|
||||||
|
|
||||||
|
function gameOver() {
|
||||||
|
cancelAnimationFrame(animId);
|
||||||
|
setHighscore('tetris', score);
|
||||||
|
document.getElementById('final-score').textContent = 'SCORE: ' + score.toLocaleString('de-DE');
|
||||||
|
document.getElementById('hs-display').textContent = getHighscore('tetris').toLocaleString('de-DE');
|
||||||
|
overlay.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function startGame() {
|
||||||
|
board = createBoard();
|
||||||
|
score = 0; level = 1; lines = 0; dropInterval = 1000; lastTime = 0;
|
||||||
|
piece = randomPiece();
|
||||||
|
nextPiece = randomPiece();
|
||||||
|
overlay.style.display = 'none';
|
||||||
|
updateHUD();
|
||||||
|
drawNext();
|
||||||
|
animId = requestAnimationFrame(loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loop(timestamp) {
|
||||||
|
const delta = timestamp - lastTime;
|
||||||
|
if (delta > dropInterval) {
|
||||||
|
lastTime = timestamp;
|
||||||
|
const moved = { ...piece, shape: piece.shape.map(r => [...r]), y: piece.y + 1 };
|
||||||
|
if (!collides(moved, board)) {
|
||||||
|
piece = moved;
|
||||||
|
} else {
|
||||||
|
merge(piece);
|
||||||
|
clearLines();
|
||||||
|
piece = nextPiece;
|
||||||
|
nextPiece = randomPiece();
|
||||||
|
drawNext();
|
||||||
|
if (collides(piece, board)) { gameOver(); return; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drawBoard();
|
||||||
|
drawPiece(piece);
|
||||||
|
animId = requestAnimationFrame(loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', e => {
|
||||||
|
if (overlay.style.display === 'flex') return;
|
||||||
|
if (e.key === 'ArrowLeft') {
|
||||||
|
const m = { ...piece, shape: piece.shape.map(r => [...r]), x: piece.x - 1 };
|
||||||
|
if (!collides(m, board)) piece = m;
|
||||||
|
} else if (e.key === 'ArrowRight') {
|
||||||
|
const m = { ...piece, shape: piece.shape.map(r => [...r]), x: piece.x + 1 };
|
||||||
|
if (!collides(m, board)) piece = m;
|
||||||
|
} else if (e.key === 'ArrowDown') {
|
||||||
|
const m = { ...piece, shape: piece.shape.map(r => [...r]), y: piece.y + 1 };
|
||||||
|
if (!collides(m, board)) piece = m; else { merge(piece); clearLines(); piece = nextPiece; nextPiece = randomPiece(); drawNext(); if (collides(piece, board)) { gameOver(); return; } }
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
const rotated = rotate(piece.shape);
|
||||||
|
const m = { ...piece, shape: rotated };
|
||||||
|
if (!collides(m, board)) piece = m;
|
||||||
|
} else if (e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
while (true) {
|
||||||
|
const m = { ...piece, shape: piece.shape.map(r => [...r]), y: piece.y + 1 };
|
||||||
|
if (collides(m, board)) { merge(piece); clearLines(); piece = nextPiece; nextPiece = randomPiece(); drawNext(); if (collides(piece, board)) gameOver(); break; }
|
||||||
|
piece = m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
startGame();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user