Herzlich willkommen zum SELF-Treffen 2026
vom 24.04. – 26.04.2026
in Halle (Saale)
Beispiel:Mini-space-invaders.html
Aus SELFHTML-Wiki
<!DOCTYPE html> <html lang="de"> <head>
<meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>mini space invaders</title>
<style>
:root {
font-size: 100%;
}
body {
background-color: #0a0a1a;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 95vh;
font-family: 'Courier New', Courier, monospace;
font-size: 1rem;
color: #e0e0ff;
user-select: none;
}
#spielflaeche {
border: 0.125em solid #334;
box-shadow: 0 0 2em #1a1a5588, 0 0 4em #0a0a3388;
display: block;
background-color: #05050f;
cursor: none;
}
#steuerung {
font-size: .9em;
color: #aabbdd;
}
</style>
</head> <body>
<canvas id="spielflaeche" width="480" height="560"></canvas>
← → bewegen | Leertaste schießen | Enter Neustart
<script>
document.addEventListener('DOMContentLoaded', function () {
// 1. INITIALISIERUNG
// ===============================================
const leinwand = document.getElementById('spielflaeche');
const ctx = leinwand.getContext('2d');
const BREITE = leinwand.width;
const HOEHE = leinwand.height;
// 2. SPIELKONSTANTEN // ===============================================
const SPIELER_GESCHWINDIGKEIT = 4; const SPIELER_BREITE = 36; const SPIELER_HOEHE = 22;
const LASER_GESCHWINDIGKEIT = 7; const LASER_BREITE = 3; const LASER_HOEHE = 14; const LASER_COOLDOWN_MS = 200;
const ALIEN_BREITE = 30; const ALIEN_HOEHE = 20; const ALIEN_SPALTEN = 10; const ALIEN_REIHEN = 4; const ALIEN_ABSTAND_X = 46; const ALIEN_ABSTAND_Y = 40; const ALIEN_START_X = 28; const ALIEN_START_Y = 60; const ALIEN_BOMBE_CHANCE = 0.0008; const ALIEN_GESCHWINDIGKEIT_X = 0.8; const ALIEN_ABSTIEG = 16;
const BOMBE_GESCHWINDIGKEIT = 3; const PUNKTE_PRO_ALIEN = 10;
const HUD_RAND_X = 12; const HUD_ZEILE_Y = 24; const HUD_TRENNLINIE_Y = 34;
const BLOCK_BREITE = 48;
const BLOCK_HOEHE = 20;
const BLOCK_ANZAHL = 3;
const BLOCK_Y = HOEHE - 110;
const BLOCK_TREFFER = 3;
const BLOCK_FARBEN = { 3: '#44cc66', 2: '#ccaa22', 1: '#cc4422' };
const SCHRIFT_HUD = '16px "Courier New", Courier, monospace'; const SCHRIFT_KLEIN = '15px "Courier New", Courier, monospace'; const SCHRIFT_TITEL = 'bold 32px "Courier New", Courier, monospace'; const SCHRIFT_START = 'bold 36px "Courier New", Courier, monospace'; const SCHRIFT_EMOJI = '80px monospace';
// 3. SPIELZUSTAND – veränderliche Daten // ===============================================
let zustand = erstelleNeuesSpiel(); let rafId = null;
function erstelleNeuesSpiel() {
return {
aktiv: false,
spielVorbei: false,
gewonnen: false,
punkte: 0,
leben: 3,
spieler: {
x: BREITE / 2 - SPIELER_BREITE / 2,
y: HOEHE - 50,
breite: SPIELER_BREITE,
hoehe: SPIELER_HOEHE,
links: false,
rechts: false,
letzterSchuss: 0,
},
laser: [],
bomben: [],
aliens: [],
bloecke: (() => {
const bloecke = [];
const abstand = BREITE / (BLOCK_ANZAHL + 1);
for (let i = 0; i < BLOCK_ANZAHL; i++) {
bloecke.push({
x: Math.round(abstand * (i + 1) - BLOCK_BREITE / 2),
y: BLOCK_Y,
breite: BLOCK_BREITE,
hoehe: BLOCK_HOEHE,
treffer: BLOCK_TREFFER,
});
}
return bloecke;
})(),
alienRichtung: 1,
sterne: erzeugeSterne(80),
};
}
// 4. ALIEN-FORMATION aufbauen // ===============================================
function initAliens() {
const reihenFarben = ['#ff6b6b', '#ffaa55', '#ffee55', '#55ffaa'];
zustand.aliens = [];
for (let reihe = 0; reihe < ALIEN_REIHEN; reihe++) {
for (let spalte = 0; spalte < ALIEN_SPALTEN; spalte++) {
zustand.aliens.push({
x: ALIEN_START_X + spalte * ALIEN_ABSTAND_X,
y: ALIEN_START_Y + reihe * ALIEN_ABSTAND_Y,
breite: ALIEN_BREITE,
hoehe: ALIEN_HOEHE,
lebt: true,
farbe: reihenFarben[reihe],
});
}
}
}
// 5. HILFSFUNKTIONEN // ===============================================
function erzeugeSterne(anzahl) {
return Array.from({ length: anzahl }, () => ({
x: Math.random() * BREITE,
y: Math.random() * HOEHE,
helligkeit: 0.3 + Math.random() * 0.7,
}));
}
function kollidieren(a, b) {
return (
a.x < b.x + b.breite &&
a.x + a.breite > b.x &&
a.y < b.y + b.hoehe &&
a.y + a.hoehe > b.y
);
}
const jetztMs = () => performance.now();
// 6. EINGABE – Tastaturereignisse // ===============================================
document.addEventListener('keydown', (ereignis) => {
switch (ereignis.code) {
case 'ArrowLeft':
case 'KeyA':
zustand.spieler.links = true; break;
case 'ArrowRight':
case 'KeyD':
zustand.spieler.rechts = true; break;
case 'Space':
ereignis.preventDefault();
if (!zustand.aktiv && !zustand.spielVorbei && !zustand.gewonnen) spielStarten();
else if (zustand.aktiv) schiessen();
break;
case 'Enter':
if (zustand.spielVorbei || zustand.gewonnen) spielNeustart();
else if (!zustand.aktiv) spielStarten();
break;
}
});
document.addEventListener('keyup', (ereignis) => {
if (ereignis.code === 'ArrowLeft' || ereignis.code === 'KeyA') zustand.spieler.links = false;
if (ereignis.code === 'ArrowRight' || ereignis.code === 'KeyD') zustand.spieler.rechts = false;
});
// 7. SPIELAKTIONEN // ===============================================
function spielStarten() {
if (rafId !== null) return;
initAliens();
zustand.aktiv = true;
rafId = requestAnimationFrame(spielSchleife);
}
function spielNeustart() {
if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null; }
zustand = erstelleNeuesSpiel();
startbildschirmZeichnen();
}
function schiessen() {
const jetzt = jetztMs();
const sp = zustand.spieler;
if (jetzt - sp.letzterSchuss >= LASER_COOLDOWN_MS) {
zustand.laser.push({
x: sp.x + sp.breite / 2 - LASER_BREITE / 2,
y: sp.y,
breite: LASER_BREITE,
hoehe: LASER_HOEHE,
});
sp.letzterSchuss = jetzt;
}
}
// 8. SPIELSCHLEIFE – Herzstück // ===============================================
function spielSchleife() {
if (!zustand.aktiv) { rafId = null; return; }
aktualisieren();
zeichnen();
rafId = requestAnimationFrame(spielSchleife);
}
// 9. UPDATE – gesamte Spiellogik pro Frame // ===============================================
function aktualisieren() {
const sp = zustand.spieler;
if (sp.links && sp.x > 0) sp.x -= SPIELER_GESCHWINDIGKEIT;
if (sp.rechts && sp.x < BREITE - sp.breite) sp.x += SPIELER_GESCHWINDIGKEIT;
for (let i = zustand.laser.length - 1; i >= 0; i--) {
zustand.laser[i].y -= LASER_GESCHWINDIGKEIT;
if (zustand.laser[i].y + LASER_HOEHE < 0) zustand.laser.splice(i, 1);
}
aliensBewegen();
alienBombenWerfen();
bombenBewegen();
kollisionenPruefen();
if (zustand.aliens.every(a => !a.lebt)) {
zustand.aktiv = false;
zustand.gewonnen = true;
}
}
function aliensBewegen() {
const lebendeAliens = zustand.aliens.filter(a => a.lebt);
if (lebendeAliens.length === 0) return;
const tempo = ALIEN_GESCHWINDIGKEIT_X *
(1 + (1 - lebendeAliens.length / (ALIEN_REIHEN * ALIEN_SPALTEN)));
let linksRand = Infinity;
let rechtsRand = -Infinity;
for (const a of lebendeAliens) {
if (a.x < linksRand) linksRand = a.x;
if (a.x + a.breite > rechtsRand) rechtsRand = a.x + a.breite;
}
let richtungsWechsel = false;
if (rechtsRand >= BREITE && zustand.alienRichtung > 0) richtungsWechsel = true;
if (linksRand <= 0 && zustand.alienRichtung < 0) richtungsWechsel = true;
if (richtungsWechsel) {
zustand.alienRichtung *= -1;
zustand.aliens.forEach(a => { if (a.lebt) a.y += ALIEN_ABSTIEG; });
} else {
zustand.aliens.forEach(a => { if (a.lebt) a.x += tempo * zustand.alienRichtung; });
}
zustand.aliens.forEach(a => {
if (a.lebt && a.y + a.hoehe >= zustand.spieler.y) {
zustand.aktiv = false;
zustand.spielVorbei = true;
}
});
}
function alienBombenWerfen() {
const untersteAliens = [];
for (let spalte = 0; spalte < ALIEN_SPALTEN; spalte++) {
const dieseSpalte = zustand.aliens.filter(
a => a.lebt && Math.floor((a.x - ALIEN_START_X) / ALIEN_ABSTAND_X + 0.5) === spalte
);
if (dieseSpalte.length > 0) {
untersteAliens.push(dieseSpalte.reduce((max, a) => a.y > max.y ? a : max));
}
}
untersteAliens.forEach(alien => {
if (Math.random() < ALIEN_BOMBE_CHANCE) {
zustand.bomben.push({
x: alien.x + alien.breite / 2 - 2,
y: alien.y + alien.hoehe,
breite: 4,
hoehe: 10,
});
}
});
}
function bombenBewegen() {
for (let i = zustand.bomben.length - 1; i >= 0; i--) {
zustand.bomben[i].y += BOMBE_GESCHWINDIGKEIT;
if (zustand.bomben[i].y > HOEHE) zustand.bomben.splice(i, 1);
}
}
// Prüft alle relevanten Kollisionen:
function kollisionenPruefen() {
const sp = zustand.spieler;
// 1. Laser ↔ Alien
for (let li = zustand.laser.length - 1; li >= 0; li--) {
for (let ai = 0; ai < zustand.aliens.length; ai++) {
const alien = zustand.aliens[ai];
if (!alien.lebt) continue;
if (kollidieren(zustand.laser[li], alien)) {
alien.lebt = false;
zustand.laser.splice(li, 1);
zustand.punkte += PUNKTE_PRO_ALIEN;
break;
}
}
}
// 2. Laser ↔ Block
for (let li = zustand.laser.length - 1; li >= 0; li--) {
for (const block of zustand.bloecke) {
if (block.treffer <= 0) continue;
if (kollidieren(zustand.laser[li], block)) {
block.treffer--;
zustand.laser.splice(li, 1);
break;
}
}
}
// 3. Bombe ↔ Block
for (let bi = zustand.bomben.length - 1; bi >= 0; bi--) {
for (const block of zustand.bloecke) {
if (block.treffer <= 0) continue;
if (kollidieren(zustand.bomben[bi], block)) {
block.treffer--;
zustand.bomben.splice(bi, 1);
break;
}
}
}
// 4. Bombe ↔ Spieler
for (let bi = zustand.bomben.length - 1; bi >= 0; bi--) {
if (kollidieren(zustand.bomben[bi], sp)) {
zustand.bomben.splice(bi, 1);
zustand.leben--;
if (zustand.leben <= 0) {
zustand.aktiv = false;
zustand.spielVorbei = true;
}
}
}
}
// 10. ZEICHENFUNKTIONEN // ===============================================
function zeichnen() {
ctx.clearRect(0, 0, BREITE, HOEHE);
sterneFeldZeichnen();
bloeckeZeichnen();
spielerZeichnen();
lasersZeichnen();
aliensZeichnen();
bombenZeichnen();
hudZeichnen();
if (zustand.spielVorbei) spielEndeOverlay(false);
else if (zustand.gewonnen) spielEndeOverlay(true);
}
function sterneFeldZeichnen() {
zustand.sterne.forEach(stern => {
ctx.globalAlpha = stern.helligkeit;
ctx.fillStyle = '#ffffff';
ctx.fillRect(stern.x, stern.y, 1.5, 1.5);
});
ctx.globalAlpha = 1;
}
function bloeckeZeichnen() {
zustand.bloecke.forEach(block => {
if (block.treffer <= 0) return;
ctx.fillStyle = BLOCK_FARBEN[block.treffer];
ctx.fillRect(block.x, block.y, block.breite, block.hoehe);
if (block.treffer < BLOCK_TREFFER) {
ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)';
ctx.lineWidth = 2;
const risse = BLOCK_TREFFER - block.treffer;
for (let r = 0; r < risse; r++) {
const rissY = block.y + Math.round(block.hoehe * (r + 1) / (risse + 1));
ctx.beginPath();
ctx.moveTo(block.x, rissY);
ctx.lineTo(block.x + block.breite, rissY);
ctx.stroke();
}
}
});
}
function spielerZeichnen() {
const sp = zustand.spieler;
ctx.fillStyle = '#4af';
ctx.fillRect(sp.x + 12, sp.y + 4, 12, 14);
ctx.fillStyle = '#9ef';
ctx.fillRect(sp.x + 15, sp.y, 6, 8);
ctx.fillStyle = '#4af';
ctx.fillRect(sp.x, sp.y + 10, 14, 8);
ctx.fillRect(sp.x + 22, sp.y + 10, 14, 8);
ctx.fillStyle = '#06f';
ctx.fillRect(sp.x + 14, sp.y + 18, 8, 4);
}
function lasersZeichnen() {
zustand.laser.forEach(l => {
ctx.fillStyle = '#aaf';
ctx.globalAlpha = 0.3;
ctx.fillRect(l.x - 2, l.y, LASER_BREITE + 4, LASER_HOEHE);
ctx.globalAlpha = 1;
ctx.fillStyle = '#fff';
ctx.fillRect(l.x, l.y, LASER_BREITE, LASER_HOEHE);
});
}
function aliensZeichnen() {
zustand.aliens.forEach(alien => {
if (!alien.lebt) return;
const x = alien.x;
const y = alien.y;
ctx.fillStyle = alien.farbe;
ctx.fillRect(x + 6, y, 18, 4);
ctx.fillRect(x + 2, y + 4, 26, 8);
ctx.fillRect(x + 6, y + 12, 18, 6);
ctx.fillStyle = '#fff';
ctx.fillRect(x + 8, y + 5, 4, 4);
ctx.fillRect(x + 18, y + 5, 4, 4);
ctx.fillStyle = alien.farbe;
ctx.fillRect(x + 4, y - 4, 4, 5);
ctx.fillRect(x + 22, y - 4, 4, 5);
ctx.fillRect(x + 4, y + 18, 5, 4);
ctx.fillRect(x + 21, y + 18, 5, 4);
});
}
function bombenZeichnen() {
zustand.bomben.forEach(b => {
ctx.fillStyle = '#f55';
ctx.fillRect(b.x, b.y, b.breite, b.hoehe);
ctx.fillStyle = '#f884';
ctx.globalAlpha = 0.4;
ctx.fillRect(b.x - 2, b.y - 2, b.breite + 4, b.hoehe + 4);
ctx.globalAlpha = 1;
});
}
function hudZeichnen() {
ctx.font = SCHRIFT_HUD;
ctx.fillStyle = '#ffffff';
ctx.fillText(`PUNKTE: ${zustand.punkte}`, HUD_RAND_X, HUD_ZEILE_Y);
ctx.fillStyle = '#ff7777';
ctx.textAlign = 'right';
ctx.fillText(`LEBEN: ${'♥ '.repeat(zustand.leben).trim()}`, BREITE - HUD_RAND_X, HUD_ZEILE_Y);
ctx.textAlign = 'left';
ctx.strokeStyle = '#445';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, HUD_TRENNLINIE_Y);
ctx.lineTo(BREITE, HUD_TRENNLINIE_Y);
ctx.stroke();
}
function spielEndeOverlay(gewonnen) {
const mitteX = BREITE / 2;
const mitteY = HOEHE / 2;
ctx.fillStyle = 'rgba(0, 0, 20, 0.80)';
ctx.fillRect(0, 0, BREITE, HOEHE);
ctx.textAlign = 'center';
ctx.font = SCHRIFT_TITEL;
ctx.fillStyle = gewonnen ? '#66ffaa' : '#ff6666';
ctx.fillText(gewonnen ? '⚡ gewonnen!' : '✕ game over', mitteX, mitteY - 30);
ctx.font = SCHRIFT_HUD;
ctx.fillStyle = '#ffffff';
ctx.fillText(`Punkte: ${zustand.punkte}`, mitteX, mitteY + 12);
ctx.font = SCHRIFT_KLEIN;
ctx.fillStyle = '#aabbcc';
ctx.fillText('[ Enter ] nochmal spielen', mitteX, mitteY + 46);
ctx.textAlign = 'left'; }
function startbildschirmZeichnen() {
ctx.clearRect(0, 0, BREITE, HOEHE);
sterneFeldZeichnen();
const mitteX = BREITE / 2;
const mitteY = HOEHE / 2;
ctx.textAlign = 'center';
ctx.font = SCHRIFT_START;
ctx.fillStyle = '#7fdfff';
ctx.fillText('mini space invaders', mitteX, mitteY - 80);
ctx.font = SCHRIFT_EMOJI;
ctx.fillText('👾', mitteX, mitteY + 10);
ctx.font = SCHRIFT_HUD;
ctx.fillStyle = '#ccddef';
ctx.fillText('← → bewegen | Leertaste schießen', mitteX, mitteY + 60);
ctx.font = SCHRIFT_KLEIN;
ctx.fillStyle = '#66ffaa';
ctx.fillText('[ Enter oder Leertaste ] starten', mitteX, mitteY + 90);
ctx.textAlign = 'left'; }
// 11. SPIELSTART // ===============================================
startbildschirmZeichnen();
}); </script>
</body> </html>