Hallo zusammen,
ich habe mal die „künstliche Inkompetenz“ herausgefordert, mit der Aufgabe: „kannst du für das lovelace-dashbord von home assistant eine karte erstellen, die wie das spiel tetris funktioniert“
Dabei ist das dabei herausgekommen
Der folgende Code muss als tetris.html im www - Verzeichnis abgelegt werden.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>HA Tetris Pro - Final Layout</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<style>
:root {
--primary-bg: #101216;
--card-bg: #1c2028;
--text-color: #e1e1e1;
--accent-color: #03a9f4;
--danger-color: #f44336;
--border-radius: 12px;
}
body {
background-color: var(--primary-bg);
color: var(--text-color);
font-family: 'Roboto', sans-serif;
margin: 0;
display: flex;
flex-direction: column;
height: 100vh;
width: 100vw;
overflow: hidden;
}
#header {
flex: 0 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 15px;
background-color: var(--card-bg);
box-shadow: 0 2px 10px rgba(0,0,0,0.5);
z-index: 20;
}
.stats-group { display: flex; gap: 12px; }
.stat-box { font-weight: bold; font-size: 0.8em; line-height: 1.2; }
.stat-box span { color: var(--accent-color); font-size: 1.1em; display: block; }
#reset-btn {
background-color: transparent;
color: var(--danger-color);
border: 1px solid var(--danger-color);
border-radius: 6px;
padding: 4px 8px;
cursor: pointer;
font-weight: bold;
font-size: 0.75em;
}
/* Hauptcontainer mit verbesserter Platzverteilung */
#main-content {
flex: 1 1 auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start; /* Startet oben */
padding: 10px;
min-height: 0;
box-sizing: border-box;
}
#game-container {
flex: 1 1 auto; /* Nimmt verfĂĽgbaren Platz ein */
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 0; /* Erlaubt dem Canvas zu schrumpfen */
position: relative;
margin-bottom: 10px; /* Sicherheitsabstand zu den Buttons */
}
canvas {
max-height: 100%;
max-width: 100%;
aspect-ratio: 240 / 400;
background-color: #000;
border-radius: var(--border-radius);
border: 2px solid #333;
box-shadow: 0 4px 15px rgba(0,0,0,0.6);
object-fit: contain;
}
#pause-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.7);
display: none;
align-items: center;
justify-content: center;
font-size: 2em;
font-weight: bold;
color: var(--accent-color);
border-radius: var(--border-radius);
pointer-events: none;
}
/* Feste Steuerung am unteren Rand */
#controls {
flex: 0 0 auto;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
width: 100%;
max-width: 300px;
padding: 5px 0 15px 0;
z-index: 20;
}
.btn {
background-color: var(--card-bg);
color: var(--text-color);
border: 1px solid #444;
border-radius: var(--border-radius);
font-size: 1.3em;
padding: 14px;
text-align: center;
cursor: pointer;
user-select: none;
box-shadow: 0 3px 5px rgba(0,0,0,0.3);
}
.btn:active { background-color: var(--accent-color); transform: scale(0.95); }
.btn-rotate { grid-column: 2; grid-row: 1; border-color: var(--accent-color); color: var(--accent-color); }
.btn-left { grid-column: 1; grid-row: 2; }
.btn-down { grid-column: 2; grid-row: 2; }
.btn-right { grid-column: 3; grid-row: 2; }
</style>
</head>
<body>
<div id="header">
<div class="stats-group">
<div class="stat-box">Score <span id="score">0</span></div>
<div class="stat-box">Lvl <span id="level">1</span></div>
</div>
<button id="reset-btn" onclick="confirmReset()">NEU</button>
</div>
<div id="main-content">
<div id="game-container" onclick="togglePause()">
<canvas id="tetris" width="240" height="400"></canvas>
<div id="pause-overlay">PAUSE</div>
</div>
<div id="controls">
<div class="btn btn-rotate" onclick="playerRotate(1)">↻</div>
<div class="btn btn-left" onclick="playerMove(-1)">â—€</div>
<div class="btn btn-down" onclick="playerDrop()">â–Ľ</div>
<div class="btn btn-right" onclick="playerMove(1)">â–¶</div>
</div>
</div>
<script>
const canvas = document.getElementById('tetris');
const context = canvas.getContext('2d');
const pauseOverlay = document.getElementById('pause-overlay');
context.scale(20, 20);
let paused = false;
let dropCounter = 0;
let dropInterval = 1000;
let lastTime = 0;
const colors = [null, '#FF4081', '#00E5FF', '#00E676', '#D500F9', '#FF9100', '#FFEA00', '#2979FF'];
const arena = createMatrix(12, 20);
const player = { pos: {x: 0, y: 0}, matrix: null, score: 0, lines: 0, level: 1 };
function togglePause() {
paused = !paused;
pauseOverlay.style.display = paused ? 'flex' : 'none';
}
function confirmReset() {
if (player.score === 0 || confirm("Neues Spiel starten?")) {
arena.forEach(row => row.fill(0));
player.score = 0; player.lines = 0; player.level = 1;
dropInterval = 1000;
paused = false;
pauseOverlay.style.display = 'none';
playerReset();
updateScore();
}
}
function arenaSweep() {
let rowCount = 0;
outer: for (let y = arena.length - 1; y > 0; --y) {
for (let x = 0; x < arena[y].length; ++x) {
if (arena[y][x] === 0) continue outer;
}
const row = arena.splice(y, 1)[0].fill(0);
arena.unshift(row);
++y; rowCount++;
}
if(rowCount > 0) {
player.score += (rowCount * 10) * player.level;
player.lines += rowCount;
player.level = Math.floor(player.lines / 10) + 1;
dropInterval = Math.max(100, 1000 - (player.level - 1) * 100);
updateScore();
}
}
function collide(arena, player) {
const [m, o] = [player.matrix, player.pos];
for (let y = 0; y < m.length; ++y) {
for (let x = 0; x < m[y].length; ++x) {
if (m[y][x] !== 0 && (arena[y + o.y] && arena[y + o.y][x + o.x]) !== 0) return true;
}
}
return false;
}
function createMatrix(w, h) {
const matrix = [];
while (h--) matrix.push(new Array(w).fill(0));
return matrix;
}
function createPiece(type) {
if (type === 'T') return [[0, 1, 0], [1, 1, 1], [0, 0, 0]];
if (type === 'O') return [[2, 2], [2, 2]];
if (type === 'L') return [[0, 3, 0], [0, 3, 0], [0, 3, 3]];
if (type === 'J') return [[0, 4, 0], [0, 4, 0], [4, 4, 0]];
if (type === 'I') return [[0, 5, 0, 0], [0, 5, 0, 0], [0, 5, 0, 0], [0, 5, 0, 0]];
if (type === 'S') return [[0, 6, 6], [6, 6, 0], [0, 0, 0]];
if (type === 'Z') return [[7, 7, 0], [0, 7, 7], [0, 0, 0]];
}
function draw() {
context.fillStyle = '#000';
context.fillRect(0, 0, canvas.width, canvas.height);
// Raster
context.lineWidth = 0.05;
context.strokeStyle = 'rgba(255,255,255,0.1)';
for (let x = 0; x <= 12; x++) { context.beginPath(); context.moveTo(x, 0); context.lineTo(x, 20); context.stroke(); }
for (let y = 0; y <= 20; y++) { context.beginPath(); context.moveTo(0, y); context.lineTo(12, y); context.stroke(); }
drawMatrix(arena, {x: 0, y: 0});
drawMatrix(player.matrix, player.pos);
}
function drawMatrix(matrix, offset) {
matrix.forEach((row, y) => {
row.forEach((value, x) => {
if (value !== 0) {
context.fillStyle = colors[value];
context.beginPath();
context.roundRect(x + offset.x, y + offset.y, 0.95, 0.95, 0.1);
context.fill();
}
});
});
}
function merge(arena, player) {
player.matrix.forEach((row, y) => {
row.forEach((value, x) => {
if (value !== 0) arena[y + player.pos.y][x + player.pos.x] = value;
});
});
}
function playerDrop() {
if (paused) return;
player.pos.y++;
if (collide(arena, player)) {
player.pos.y--; merge(arena, player);
playerReset(); arenaSweep();
}
dropCounter = 0;
}
function playerMove(dir) {
if (paused) return;
player.pos.x += dir;
if (collide(arena, player)) player.pos.x -= dir;
}
function playerReset() {
const pieces = 'ILJOTSZ';
player.matrix = createPiece(pieces[pieces.length * Math.random() | 0]);
player.pos.y = 0;
player.pos.x = (arena[0].length / 2 | 0) - (player.matrix[0].length / 2 | 0);
if (collide(arena, player)) {
arena.forEach(row => row.fill(0));
player.score = 0; player.lines = 0; player.level = 1;
dropInterval = 1000; updateScore();
}
}
function playerRotate(dir) {
if (paused) return;
const pos = player.pos.x;
let offset = 1;
rotate(player.matrix, dir);
while (collide(arena, player)) {
player.pos.x += offset;
offset = -(offset + (offset > 0 ? 1 : -1));
if (offset > player.matrix[0].length) {
rotate(player.matrix, -dir);
player.pos.x = pos; return;
}
}
}
function rotate(matrix, dir) {
for (let y = 0; y < matrix.length; ++y) {
for (let x = 0; x < y; ++x) {
[matrix[x][y], matrix[y][x]] = [matrix[y][x], matrix[x][y]];
}
}
if (dir > 0) matrix.forEach(row => row.reverse());
else matrix.reverse();
}
function update(time = 0) {
const deltaTime = time - lastTime;
lastTime = time;
if (!paused) {
dropCounter += deltaTime;
if (dropCounter > dropInterval) playerDrop();
draw();
}
requestAnimationFrame(update);
}
function updateScore() {
document.getElementById('score').innerText = player.score;
document.getElementById('level').innerText = player.level;
}
playerReset(); updateScore(); update();
</script>
</body>
</html>
Der Code fĂĽr die Karte:
type: iframe
url: /local/tetris.html?v=4
aspect_ratio: 16:9
Es gibt auch eine Pause-Funktion, indem man in das Spielfeld klickt.
Hier noch ein paar Tipps:
- Voraussetzungen: Die Datei
tetris.htmlmuss im Ordner/config/www/liegen , damit sie ĂĽber/local/tetris.html?v=4erreichbar ist. - Neustart-Hinweis: Einige Nutzer wissen evtl. nicht, dass man Home Assistant einmal neu starten muss, wenn man den
www-Ordner zum ersten Mal erstellt hat. - Anpassbarkeit: Man kann die Farben im
:root-Bereich des CSS ganz einfach an das eigene Dashboard-Theme anpassen.









