managarten/games/arcade/apps/web/static/games/puzzle_blocks.html
Till JS 9e82e40e16 rename(mana-games): rebrand to Arcade
Rename games/mana-games/ to games/arcade/, update all package names
(@mana-games/* → @arcade/*), appIds, display names, docker-compose
service, root scripts, and documentation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:31:37 +02:00

636 lines
No EOL
19 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Puzzle Blocks - Mana Games</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0a0a0a;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
overflow: hidden;
}
.game-container {
display: flex;
gap: 2rem;
align-items: flex-start;
padding: 1rem;
}
.game-board {
position: relative;
background: #1a1a1a;
border: 2px solid #333;
border-radius: 8px;
padding: 10px;
box-shadow: 0 0 30px rgba(157, 48, 255, 0.3);
}
#gameCanvas {
display: block;
background: #0a0a0a;
border-radius: 4px;
}
.side-panel {
display: flex;
flex-direction: column;
gap: 1.5rem;
min-width: 200px;
}
.info-box {
background: #1a1a1a;
border: 2px solid #333;
border-radius: 8px;
padding: 1.5rem;
text-align: center;
}
.info-box h3 {
color: #9d30ff;
margin-bottom: 0.5rem;
font-size: 1.1rem;
text-transform: uppercase;
letter-spacing: 1px;
}
.score {
font-size: 2rem;
font-weight: bold;
color: #fff;
margin-bottom: 0.5rem;
}
.level, .lines {
font-size: 1.2rem;
color: #aaa;
margin: 0.3rem 0;
}
.next-piece {
background: #0a0a0a;
border-radius: 4px;
padding: 1rem;
min-height: 100px;
display: flex;
justify-content: center;
align-items: center;
}
#nextCanvas {
display: block;
}
.controls {
font-size: 0.9rem;
line-height: 1.6;
color: #aaa;
}
.controls kbd {
background: #333;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-family: monospace;
color: #9d30ff;
margin: 0 0.2rem;
}
.game-over {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(26, 26, 26, 0.95);
border: 2px solid #9d30ff;
border-radius: 12px;
padding: 2rem 3rem;
text-align: center;
display: none;
z-index: 100;
box-shadow: 0 0 50px rgba(157, 48, 255, 0.5);
}
.game-over h2 {
color: #9d30ff;
font-size: 2.5rem;
margin-bottom: 1rem;
}
.game-over p {
font-size: 1.2rem;
margin-bottom: 1.5rem;
color: #aaa;
}
.restart-btn {
background: #9d30ff;
color: white;
border: none;
padding: 0.8rem 2rem;
font-size: 1.1rem;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 1px;
}
.restart-btn:hover {
background: #7a20cc;
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(157, 48, 255, 0.5);
}
.start-screen {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(26, 26, 26, 0.95);
border: 2px solid #9d30ff;
border-radius: 12px;
padding: 2rem 3rem;
text-align: center;
z-index: 100;
box-shadow: 0 0 50px rgba(157, 48, 255, 0.5);
}
.start-screen h1 {
color: #9d30ff;
font-size: 3rem;
margin-bottom: 1rem;
text-shadow: 0 0 20px rgba(157, 48, 255, 0.5);
}
.start-btn {
background: #9d30ff;
color: white;
border: none;
padding: 1rem 3rem;
font-size: 1.2rem;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 2px;
margin-top: 1rem;
}
.start-btn:hover {
background: #7a20cc;
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(157, 48, 255, 0.5);
}
@media (max-width: 768px) {
.game-container {
flex-direction: column;
gap: 1rem;
}
.side-panel {
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
min-width: auto;
gap: 1rem;
}
.info-box {
flex: 1;
min-width: 150px;
}
}
</style>
</head>
<body>
<div class="game-container">
<div class="game-board">
<canvas id="gameCanvas"></canvas>
<div class="start-screen" id="startScreen">
<h1>PUZZLE BLOCKS</h1>
<p style="color: #aaa; margin-bottom: 1rem;">Klassisches Tetris-Gameplay</p>
<button class="start-btn" onclick="startGame()">SPIEL STARTEN</button>
</div>
<div class="game-over" id="gameOverScreen">
<h2>GAME OVER</h2>
<p>Deine Punkte: <span id="finalScore">0</span></p>
<button class="restart-btn" onclick="resetGame()">Neues Spiel</button>
</div>
</div>
<div class="side-panel">
<div class="info-box">
<h3>Punkte</h3>
<div class="score" id="score">0</div>
</div>
<div class="info-box">
<h3>Level</h3>
<div class="level">Level <span id="level">1</span></div>
<div class="lines">Linien: <span id="lines">0</span></div>
</div>
<div class="info-box">
<h3>Nächster Block</h3>
<div class="next-piece">
<canvas id="nextCanvas"></canvas>
</div>
</div>
<div class="info-box controls">
<h3>Steuerung</h3>
<p><kbd></kbd><kbd></kbd> Bewegen</p>
<p><kbd></kbd> Schneller fallen</p>
<p><kbd></kbd> Drehen</p>
<p><kbd>Space</kbd> Sofort fallen</p>
<p><kbd>P</kbd> Pause</p>
</div>
</div>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const nextCanvas = document.getElementById('nextCanvas');
const nextCtx = nextCanvas.getContext('2d');
// Game dimensions
const COLS = 10;
const ROWS = 20;
const BLOCK_SIZE = 30;
const NEXT_BLOCK_SIZE = 20;
canvas.width = COLS * BLOCK_SIZE;
canvas.height = ROWS * BLOCK_SIZE;
nextCanvas.width = 4 * NEXT_BLOCK_SIZE;
nextCanvas.height = 4 * NEXT_BLOCK_SIZE;
// Tetromino definitions
const PIECES = [
// I-piece
{
shape: [[1,1,1,1]],
color: '#00f0f0'
},
// O-piece
{
shape: [[1,1],[1,1]],
color: '#f0f000'
},
// T-piece
{
shape: [[0,1,0],[1,1,1]],
color: '#a000f0'
},
// S-piece
{
shape: [[0,1,1],[1,1,0]],
color: '#00f000'
},
// Z-piece
{
shape: [[1,1,0],[0,1,1]],
color: '#f00000'
},
// J-piece
{
shape: [[1,0,0],[1,1,1]],
color: '#0000f0'
},
// L-piece
{
shape: [[0,0,1],[1,1,1]],
color: '#f0a000'
}
];
// Game state
let board = [];
let currentPiece = null;
let nextPiece = null;
let score = 0;
let lines = 0;
let level = 1;
let dropTime = 1000;
let lastDrop = 0;
let gameRunning = false;
let gamePaused = false;
// Initialize board
function initBoard() {
board = Array(ROWS).fill().map(() => Array(COLS).fill(0));
}
// Create a new piece
function createPiece() {
const piece = PIECES[Math.floor(Math.random() * PIECES.length)];
return {
shape: piece.shape.map(row => [...row]),
color: piece.color,
x: Math.floor(COLS / 2) - Math.floor(piece.shape[0].length / 2),
y: 0
};
}
// Rotate piece
function rotatePiece(piece) {
const rotated = [];
const rows = piece.shape.length;
const cols = piece.shape[0].length;
for (let i = 0; i < cols; i++) {
rotated[i] = [];
for (let j = rows - 1; j >= 0; j--) {
rotated[i].push(piece.shape[j][i]);
}
}
return rotated;
}
// Check collision
function isValidMove(piece, x, y, shape = piece.shape) {
for (let row = 0; row < shape.length; row++) {
for (let col = 0; col < shape[row].length; col++) {
if (shape[row][col]) {
const newX = x + col;
const newY = y + row;
if (newX < 0 || newX >= COLS || newY >= ROWS) {
return false;
}
if (newY >= 0 && board[newY][newX]) {
return false;
}
}
}
}
return true;
}
// Lock piece to board
function lockPiece() {
for (let row = 0; row < currentPiece.shape.length; row++) {
for (let col = 0; col < currentPiece.shape[row].length; col++) {
if (currentPiece.shape[row][col]) {
const x = currentPiece.x + col;
const y = currentPiece.y + row;
if (y >= 0) {
board[y][x] = currentPiece.color;
}
}
}
}
}
// Clear completed lines
function clearLines() {
let linesCleared = 0;
for (let row = ROWS - 1; row >= 0; row--) {
if (board[row].every(cell => cell !== 0)) {
board.splice(row, 1);
board.unshift(Array(COLS).fill(0));
linesCleared++;
row++; // Check the same row again
}
}
if (linesCleared > 0) {
lines += linesCleared;
score += linesCleared * 100 * level;
// Bonus for multiple lines
if (linesCleared === 4) {
score += 400 * level;
}
// Level up every 10 lines
level = Math.floor(lines / 10) + 1;
dropTime = Math.max(100, 1000 - (level - 1) * 100);
updateUI();
}
}
// Update UI elements
function updateUI() {
document.getElementById('score').textContent = score;
document.getElementById('level').textContent = level;
document.getElementById('lines').textContent = lines;
}
// Draw block
function drawBlock(ctx, x, y, color, size = BLOCK_SIZE) {
ctx.fillStyle = color;
ctx.fillRect(x * size, y * size, size - 2, size - 2);
// Add gradient for 3D effect
const gradient = ctx.createLinearGradient(
x * size, y * size,
x * size + size, y * size + size
);
gradient.addColorStop(0, 'rgba(255,255,255,0.3)');
gradient.addColorStop(1, 'rgba(0,0,0,0.3)');
ctx.fillStyle = gradient;
ctx.fillRect(x * size, y * size, size - 2, size - 2);
}
// Draw board
function drawBoard() {
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw grid
ctx.strokeStyle = '#1a1a1a';
ctx.lineWidth = 1;
for (let i = 0; i <= COLS; i++) {
ctx.beginPath();
ctx.moveTo(i * BLOCK_SIZE, 0);
ctx.lineTo(i * BLOCK_SIZE, canvas.height);
ctx.stroke();
}
for (let i = 0; i <= ROWS; i++) {
ctx.beginPath();
ctx.moveTo(0, i * BLOCK_SIZE);
ctx.lineTo(canvas.width, i * BLOCK_SIZE);
ctx.stroke();
}
// Draw locked pieces
for (let row = 0; row < ROWS; row++) {
for (let col = 0; col < COLS; col++) {
if (board[row][col]) {
drawBlock(ctx, col, row, board[row][col]);
}
}
}
}
// Draw piece
function drawPiece(ctx, piece, blockSize = BLOCK_SIZE) {
for (let row = 0; row < piece.shape.length; row++) {
for (let col = 0; col < piece.shape[row].length; col++) {
if (piece.shape[row][col]) {
drawBlock(ctx, piece.x + col, piece.y + row, piece.color, blockSize);
}
}
}
}
// Draw next piece
function drawNextPiece() {
nextCtx.fillStyle = '#0a0a0a';
nextCtx.fillRect(0, 0, nextCanvas.width, nextCanvas.height);
if (nextPiece) {
const offsetX = (4 - nextPiece.shape[0].length) / 2;
const offsetY = (4 - nextPiece.shape.length) / 2;
for (let row = 0; row < nextPiece.shape.length; row++) {
for (let col = 0; col < nextPiece.shape[row].length; col++) {
if (nextPiece.shape[row][col]) {
drawBlock(nextCtx, offsetX + col, offsetY + row, nextPiece.color, NEXT_BLOCK_SIZE);
}
}
}
}
}
// Game over
function gameOver() {
gameRunning = false;
document.getElementById('finalScore').textContent = score;
document.getElementById('gameOverScreen').style.display = 'block';
}
// Reset game
function resetGame() {
initBoard();
score = 0;
lines = 0;
level = 1;
dropTime = 1000;
updateUI();
currentPiece = createPiece();
nextPiece = createPiece();
document.getElementById('gameOverScreen').style.display = 'none';
gameRunning = true;
gamePaused = false;
gameLoop();
}
// Start game
function startGame() {
document.getElementById('startScreen').style.display = 'none';
resetGame();
}
// Game loop
function gameLoop(timestamp = 0) {
if (!gameRunning || gamePaused) return;
// Auto drop
if (timestamp - lastDrop > dropTime) {
if (isValidMove(currentPiece, currentPiece.x, currentPiece.y + 1)) {
currentPiece.y++;
} else {
lockPiece();
clearLines();
currentPiece = nextPiece;
nextPiece = createPiece();
if (!isValidMove(currentPiece, currentPiece.x, currentPiece.y)) {
gameOver();
return;
}
}
lastDrop = timestamp;
}
// Draw everything
drawBoard();
if (currentPiece) {
drawPiece(ctx, currentPiece);
}
drawNextPiece();
requestAnimationFrame(gameLoop);
}
// Keyboard controls
document.addEventListener('keydown', (e) => {
if (!gameRunning || gamePaused) return;
switch(e.key) {
case 'ArrowLeft':
if (isValidMove(currentPiece, currentPiece.x - 1, currentPiece.y)) {
currentPiece.x--;
}
break;
case 'ArrowRight':
if (isValidMove(currentPiece, currentPiece.x + 1, currentPiece.y)) {
currentPiece.x++;
}
break;
case 'ArrowDown':
if (isValidMove(currentPiece, currentPiece.x, currentPiece.y + 1)) {
currentPiece.y++;
score++;
updateUI();
}
break;
case 'ArrowUp':
const rotated = rotatePiece(currentPiece);
if (isValidMove(currentPiece, currentPiece.x, currentPiece.y, rotated)) {
currentPiece.shape = rotated;
}
break;
case ' ':
// Hard drop
while (isValidMove(currentPiece, currentPiece.x, currentPiece.y + 1)) {
currentPiece.y++;
score += 2;
}
updateUI();
break;
case 'p':
case 'P':
gamePaused = !gamePaused;
if (!gamePaused) {
gameLoop();
}
break;
}
});
// Initialize
initBoard();
updateUI();
</script>
</body>
</html>