mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-16 03:39:41 +02:00
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>
636 lines
No EOL
19 KiB
HTML
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> |