""" ============================================================= LABYRINTH-QUEST – Ein Pygame-Lernspiel für Klasse 11 ============================================================= Didaktische Ziele (Kompetenzbereiche nach Du Boulay 1989): - Schleifen (for, while) - Bedingungen (if/elif/else) - Listen & 2D-Listen (Labyrinth-Grid) - Funktionen & Modularisierung - Event-Handling (Tastatureingabe) - Kollisionserkennung - Spielzustand & Logik (State Machine) - Rekursion (Maze-Generierung via Backtracking) ============================================================= """ import pygame import random import time # ───────────────────────────────────────── # KONSTANTEN # ───────────────────────────────────────── TILE = 40 # Größe einer Zelle in Pixel COLS = 19 # Spalten (ungerade!) ROWS = 15 # Zeilen (ungerade!) WIDTH = COLS * TILE HEIGHT = ROWS * TILE + 80 # +80 für HUD-Leiste FPS = 60 # Farben C_BG = (15, 20, 35) C_WALL = (30, 50, 90) C_WALL_HL = (50, 80, 130) C_PATH = (20, 30, 55) C_PLAYER = (80, 200, 120) C_PLAYER_EYE= (15, 20, 35) C_EXIT = (255, 200, 50) C_EXIT_GLOW = (255, 230, 100) C_COIN = (255, 215, 0) C_TRAP = (220, 60, 60) C_HUD_BG = (10, 15, 28) C_TEXT = (200, 220, 255) C_TITLE = (100, 180, 255) C_WHITE = (255, 255, 255) C_GREEN = ( 60, 200, 100) C_RED = (220, 60, 60) C_OVERLAY = (0, 0, 0, 180) # Zell-Typen WALL = 1 PATH = 0 # ───────────────────────────────────────── # LABYRINTH GENERIEREN (Recursive Backtracking) # ───────────────────────────────────────── def erstelle_labyrinth(cols, rows): """ Generiert ein perfektes Labyrinth mit rekursivem Backtracking. Gibt eine 2D-Liste zurück: WALL=1, PATH=0 """ # Alles mit Wänden füllen gitter = [[WALL] * cols for _ in range(rows)] def ist_gueltig(r, c): return 0 < r < rows - 1 and 0 < c < cols - 1 def grabe(r, c): gitter[r][c] = PATH richtungen = [(0, 2), (0, -2), (2, 0), (-2, 0)] random.shuffle(richtungen) for dr, dc in richtungen: nr, nc = r + dr, c + dc if ist_gueltig(nr, nc) and gitter[nr][nc] == WALL: # Wand zwischen aktuellem und nächstem Feld entfernen gitter[r + dr // 2][c + dc // 2] = PATH grabe(nr, nc) grabe(1, 1) # Start & Ziel öffnen gitter[1][1] = PATH gitter[rows-2][cols-2] = PATH return gitter # ───────────────────────────────────────── # MÜNZEN & FALLEN PLATZIEREN # ───────────────────────────────────────── def platziere_items(gitter, anzahl_muenzen=8, anzahl_fallen=5): """ Platziert Münzen und Fallen auf freien Pfadzellen. Gibt zwei Listen mit (row, col)-Tupeln zurück. """ freie_felder = [] for r in range(len(gitter)): for c in range(len(gitter[0])): if gitter[r][c] == PATH: # Start und Ziel aussparen if (r, c) not in [(1, 1), (len(gitter)-2, len(gitter[0])-2)]: freie_felder.append((r, c)) random.shuffle(freie_felder) muenzen = freie_felder[:anzahl_muenzen] fallen = freie_felder[anzahl_muenzen:anzahl_muenzen + anzahl_fallen] return muenzen, fallen # ───────────────────────────────────────── # SPIELER-KLASSE # ───────────────────────────────────────── class Spieler: def __init__(self, reihe, spalte): self.reihe = reihe self.spalte = spalte self.punkte = 0 self.leben = 3 self.bewegt = False # für Animation self.anim_x = 0.0 # interpoliertes X (Pixel) self.anim_y = 0.0 # interpoliertes Y (Pixel) self.ziel_x = float(spalte * TILE) self.ziel_y = float(reihe * TILE) self.anim_x = self.ziel_x self.anim_y = self.ziel_y self.blick = (0, 1) # Blickrichtung (dr, dc) def bewege(self, dr, dc, gitter): """Bewegt den Spieler, wenn das Zielfeld kein WALL ist.""" nr = self.reihe + dr nc = self.spalte + dc self.blick = (dr, dc) if 0 <= nr < len(gitter) and 0 <= nc < len(gitter[0]): if gitter[nr][nc] == PATH: self.reihe = nr self.spalte = nc self.ziel_x = float(nc * TILE) self.ziel_y = float(nr * TILE) return True return False def update(self, dt): """Weiche Interpolation zur Zielposition.""" speed = 12.0 self.anim_x += (self.ziel_x - self.anim_x) * speed * dt self.anim_y += (self.ziel_y - self.anim_y) * speed * dt def zeichne(self, surface): cx = int(self.anim_x) + TILE // 2 cy = int(self.anim_y) + TILE // 2 r = TILE // 2 - 4 # Körper pygame.draw.circle(surface, C_PLAYER, (cx, cy), r) # Glanz pygame.draw.circle(surface, (130, 240, 160), (cx - r//4, cy - r//4), r//3) # Augen (folgen Blickrichtung) dr, dc = self.blick ex = cx + dc * (r // 2) ey = cy + dr * (r // 2) pygame.draw.circle(surface, C_PLAYER_EYE, (ex, ey), 4) pygame.draw.circle(surface, C_WHITE, (ex + 1, ey - 1), 2) # ───────────────────────────────────────── # ZEICHENFUNKTIONEN # ───────────────────────────────────────── def zeichne_labyrinth(surface, gitter, tick): for r in range(len(gitter)): for c in range(len(gitter[0])): rect = pygame.Rect(c * TILE, r * TILE, TILE, TILE) if gitter[r][c] == WALL: pygame.draw.rect(surface, C_WALL, rect) # Dezentes Highlight oben/links pygame.draw.line(surface, C_WALL_HL, (c*TILE, r*TILE), (c*TILE+TILE, r*TILE), 1) pygame.draw.line(surface, C_WALL_HL, (c*TILE, r*TILE), (c*TILE, r*TILE+TILE), 1) else: pygame.draw.rect(surface, C_PATH, rect) def zeichne_exit(surface, gitter, tick): er = len(gitter) - 2 ec = len(gitter[0]) - 2 puls = abs((tick % 60) - 30) / 30.0 # 0..1 pulsierend glow_r = int(TILE // 2 - 4 + puls * 5) cx = ec * TILE + TILE // 2 cy = er * TILE + TILE // 2 # Glüheffekt (größerer halbtransparenter Kreis) glow_surf = pygame.Surface((TILE * 2, TILE * 2), pygame.SRCALPHA) pygame.draw.circle(glow_surf, (*C_EXIT_GLOW, 60), (TILE, TILE), glow_r + 6) surface.blit(glow_surf, (cx - TILE, cy - TILE)) pygame.draw.circle(surface, C_EXIT, (cx, cy), TILE // 2 - 4) pygame.draw.circle(surface, C_WHITE, (cx, cy), TILE // 2 - 10) # "E" für Exit font = pygame.font.SysFont("monospace", 18, bold=True) txt = font.render("E", True, C_HUD_BG) surface.blit(txt, txt.get_rect(center=(cx, cy))) def zeichne_muenzen(surface, muenzen, tick): puls = abs((tick % 40) - 20) / 20.0 r = int(TILE // 2 - 8 + puls * 3) for (mr, mc) in muenzen: cx = mc * TILE + TILE // 2 cy = mr * TILE + TILE // 2 pygame.draw.circle(surface, C_COIN, (cx, cy), r) pygame.draw.circle(surface, C_WHITE, (cx - 2, cy - 2), r // 3) def zeichne_fallen(surface, fallen, tick): for (fr, fc) in fallen: margin = 6 rect = pygame.Rect(fc * TILE + margin, fr * TILE + margin, TILE - 2*margin, TILE - 2*margin) pygame.draw.rect(surface, C_TRAP, rect, border_radius=4) # Kreuz-Symbol cx = fc * TILE + TILE // 2 cy = fr * TILE + TILE // 2 d = TILE // 2 - margin - 2 pygame.draw.line(surface, C_WHITE, (cx-d, cy-d), (cx+d, cy+d), 2) pygame.draw.line(surface, C_WHITE, (cx+d, cy-d), (cx-d, cy+d), 2) def zeichne_hud(surface, spieler, level, zeit_rest, font, font_klein): hud_rect = pygame.Rect(0, ROWS * TILE, WIDTH, 80) pygame.draw.rect(surface, C_HUD_BG, hud_rect) pygame.draw.line(surface, C_WALL_HL, (0, ROWS*TILE), (WIDTH, ROWS*TILE), 2) y = ROWS * TILE + 10 # Level txt = font.render(f"Level {level}", True, C_TITLE) surface.blit(txt, (10, y)) # Punkte txt = font.render(f"Punkte: {spieler.punkte}", True, C_TEXT) surface.blit(txt, (10, y + 30)) # Leben (Herzen) herz_txt = font_klein.render("Leben: " + "♥ " * spieler.leben, True, C_RED) surface.blit(herz_txt, (WIDTH // 2 - herz_txt.get_width() // 2, y + 5)) # Zeit farbe = C_GREEN if zeit_rest > 15 else C_RED txt = font.render(f"Zeit: {int(zeit_rest)}s", True, farbe) surface.blit(txt, (WIDTH - txt.get_width() - 10, y)) # Steuerung-Hinweis hint = font_klein.render("Pfeiltasten / WASD zum Bewegen | ESC: Beenden", True, (80, 100, 140)) surface.blit(hint, (WIDTH // 2 - hint.get_width() // 2, y + 50)) def zeige_overlay(surface, zeilen, font_gross, font): """Halbtransparentes Overlay mit zentriertem Text.""" overlay = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA) overlay.fill((0, 0, 0, 200)) surface.blit(overlay, (0, 0)) for i, (text, farbe, gross) in enumerate(zeilen): f = font_gross if gross else font txt = f.render(text, True, farbe) y = HEIGHT // 2 - len(zeilen) * 20 + i * 50 surface.blit(txt, txt.get_rect(center=(WIDTH // 2, y))) # ───────────────────────────────────────── # SPIELZUSTÄNDE (State Machine) # ───────────────────────────────────────── ZUSTAND_TITEL = "titel" ZUSTAND_SPIEL = "spiel" ZUSTAND_GEWINN = "gewinn" ZUSTAND_VERLOR = "verloren" ZUSTAND_PAUSE = "pause" ZEIT_LIMIT = 60 # Sekunden pro Level def spiel_starten(level): """Gibt alle Spielvariablen für ein neues Level zurück.""" gitter = erstelle_labyrinth(COLS, ROWS) muenzen, fallen = platziere_items(gitter, anzahl_muenzen=6 + level * 2, anzahl_fallen=3 + level) spieler = Spieler(1, 1) start_zeit = time.time() return gitter, muenzen, fallen, spieler, start_zeit # ───────────────────────────────────────── # HAUPTPROGRAMM # ───────────────────────────────────────── def main(): pygame.init() pygame.display.set_caption("Labyrinth-Quest – Klasse 11 Informatik") screen = pygame.display.set_mode((WIDTH, HEIGHT)) clock = pygame.time.Clock() # Schriftarten try: font_gross = pygame.font.SysFont("monospace", 42, bold=True) font = pygame.font.SysFont("monospace", 24, bold=True) font_klein = pygame.font.SysFont("monospace", 16) except: font_gross = pygame.font.Font(None, 48) font = pygame.font.Font(None, 28) font_klein = pygame.font.Font(None, 20) # Spielstart-Variablen zustand = ZUSTAND_TITEL level = 1 gesamt_pkt = 0 gitter = muenzen = fallen = spieler = start_zeit = None tick = 0 running = True while running: dt = clock.tick(FPS) / 1000.0 tick += 1 # ── Events ────────────────────────────────── for event in pygame.event.get(): if event.type == pygame.QUIT: running = False elif event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: if zustand == ZUSTAND_SPIEL: zustand = ZUSTAND_PAUSE elif zustand == ZUSTAND_PAUSE: zustand = ZUSTAND_SPIEL else: running = False # Titelschirm → Spiel starten elif zustand == ZUSTAND_TITEL and event.key in ( pygame.K_RETURN, pygame.K_SPACE): level = 1 gitter, muenzen, fallen, spieler, start_zeit = spiel_starten(level) zustand = ZUSTAND_SPIEL # Nach Gewinn / Verlieren → weiter oder neu elif zustand in (ZUSTAND_GEWINN, ZUSTAND_VERLOR): if event.key == pygame.K_RETURN: if zustand == ZUSTAND_GEWINN: level += 1 gesamt_pkt += spieler.punkte else: level = 1 gesamt_pkt = 0 gitter, muenzen, fallen, spieler, start_zeit = spiel_starten(level) zustand = ZUSTAND_SPIEL elif event.key == pygame.K_ESCAPE: zustand = ZUSTAND_TITEL # Pause → weiter elif zustand == ZUSTAND_PAUSE and event.key == pygame.K_RETURN: zustand = ZUSTAND_SPIEL # Spielerbewegung elif zustand == ZUSTAND_SPIEL: dr, dc = 0, 0 if event.key in (pygame.K_UP, pygame.K_w): dr, dc = -1, 0 if event.key in (pygame.K_DOWN, pygame.K_s): dr, dc = 1, 0 if event.key in (pygame.K_LEFT, pygame.K_a): dr, dc = 0, -1 if event.key in (pygame.K_RIGHT, pygame.K_d): dr, dc = 0, 1 if (dr, dc) != (0, 0): spieler.bewege(dr, dc, gitter) # ── Spiellogik ────────────────────────────── if zustand == ZUSTAND_SPIEL and spieler: spieler.update(dt) pos = (spieler.reihe, spieler.spalte) # Münze einsammeln if pos in muenzen: muenzen.remove(pos) spieler.punkte += 10 # Falle auslösen if pos in fallen: fallen.remove(pos) spieler.leben -= 1 spieler.punkte = max(0, spieler.punkte - 5) if spieler.leben <= 0: zustand = ZUSTAND_VERLOR # Ziel erreicht if pos == (ROWS - 2, COLS - 2): spieler.punkte += 50 + len(muenzen) * 5 # Bonus für übrige Münzen zustand = ZUSTAND_GEWINN # Zeitlimit zeit_vergangen = time.time() - start_zeit zeit_rest = max(0, ZEIT_LIMIT - (level - 1) * 5 - zeit_vergangen) if zeit_rest <= 0: spieler.leben -= 1 if spieler.leben <= 0: zustand = ZUSTAND_VERLOR else: # Neues Labyrinth, Leben-Abzug gitter, muenzen, fallen, spieler_neu, start_zeit = spiel_starten(level) spieler_neu.leben = spieler.leben spieler_neu.punkte = spieler.punkte spieler = spieler_neu else: zeit_rest = ZEIT_LIMIT # ── Zeichnen ──────────────────────────────── screen.fill(C_BG) if zustand == ZUSTAND_TITEL: # Titelschirm screen.fill(C_BG) zeige_overlay(screen, [ ("LABYRINTH-QUEST", C_TITLE, True), ("Finde den Ausgang!", C_TEXT, False), ("Sammle Münzen ♦ Meide Fallen ✕", C_COIN, False), ("", C_TEXT, False), ("[ENTER] Starten", C_GREEN, False), ("[ESC] Beenden", C_RED, False), ], font_gross, font) elif zustand in (ZUSTAND_SPIEL, ZUSTAND_PAUSE): zeichne_labyrinth(screen, gitter, tick) zeichne_exit(screen, gitter, tick) zeichne_muenzen(screen, muenzen, tick) zeichne_fallen(screen, fallen, tick) spieler.zeichne(screen) zeichne_hud(screen, spieler, level, zeit_rest, font, font_klein) if zustand == ZUSTAND_PAUSE: zeige_overlay(screen, [ ("PAUSE", C_TITLE, True), ("[ENTER] Weiter", C_GREEN, False), ("[ESC] Beenden", C_RED, False), ], font_gross, font) elif zustand == ZUSTAND_GEWINN: zeichne_labyrinth(screen, gitter, tick) zeichne_exit(screen, gitter, tick) spieler.zeichne(screen) zeichne_hud(screen, spieler, level, 0, font, font_klein) zeige_overlay(screen, [ ("LEVEL GESCHAFFT! 🎉", C_COIN, True), (f"Punkte: {spieler.punkte}", C_TEXT, False), (f"Gesamt: {gesamt_pkt + spieler.punkte}", C_GREEN, False), ("[ENTER] Nächstes Level", C_TITLE, False), ("[ESC] Zum Titel", C_RED, False), ], font_gross, font) elif zustand == ZUSTAND_VERLOR: zeichne_labyrinth(screen, gitter, tick) spieler.zeichne(screen) zeichne_hud(screen, spieler, level, 0, font, font_klein) zeige_overlay(screen, [ ("GAME OVER", C_RED, True), (f"Punkte: {spieler.punkte}", C_TEXT, False), ("[ENTER] Neu starten", C_GREEN, False), ("[ESC] Zum Titel", C_TITLE, False), ], font_gross, font) pygame.display.flip() pygame.quit() if __name__ == "__main__": main()