Sitzung 4: Erstellen eines Spiels mithilfe von KI. Signed-off-by: glmacedo <gian.mocerinomacedo@stud.ph-weingarten.de>
474 lines
No EOL
19 KiB
Python
474 lines
No EOL
19 KiB
Python
"""
|
||
=============================================================
|
||
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() |