diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c300f74 --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*.so +*.pyd +*.pyo + +# Virtual environments +.venv/ +venv/ +ENV/ + +# Jupyter Notebook checkpoints +.ipynb_checkpoints/ + +# Raster and geodata outputs +*.tif +*.npy +*.png +trinkbrunnen_pixel_positions.npy +trinkbrunnen_karte.png + +# Matplotlib figures +*.pdf +*.svg + +# OS files +.DS_Store +Thumbs.db + +# Editor files +.vscode/ +.idea/ + +# Logs +*.log + +# Test and coverage +.coverage +htmlcov/ + +# Misc +*.bak +*.tmp + +# Ignore results folder except source data +results/ +!results/s2_2025.tif + +# Ignore geodata except source files +geodata/ +!geodata/Trinkbrunnen_Berlin.geojson + +# PyInstaller build outputs +build/ +dist/ +*.spec + +# Executables +*.exe +*.app diff --git a/README.md b/README.md index e69de29..de5b44d 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,30 @@ +# Trinkbrunnen Berlin – Top-Down Spiel + +Dieses Projekt ist ein einfaches Top-Down-Spiel mit einer echten Satellitenkarte von Berlin als Hintergrund. Die Trinkbrunnen-Standorte werden aus einer GeoJSON-Datei geladen und auf der Karte angezeigt. Du kannst die Brunnen im Spiel nutzen und mit ihnen interagieren. + +## Schnellstart + +1. Python 3 installieren +2. Abhängigkeiten installieren: + + ``` + pip install -r requirements.txt + ``` + +3. Spiel starten: + + ``` + python water-game.py + ``` + +## Features + +- Berlin-Satellitenkarte als Spielfeld +- Trinkbrunnen werden korrekt angezeigt +- Interaktion mit Trinkbrunnen möglich +- Minimap und Gegner + +## Datenquellen + +- `geodata/s2_2025.tif`: Satellitenbild Berlin +- `geodata/Trinkbrunnen_Berlin.geojson`: Trinkbrunnen-Standorte diff --git a/README_TIPS.md b/README_TIPS.md new file mode 100644 index 0000000..3dbc291 --- /dev/null +++ b/README_TIPS.md @@ -0,0 +1,174 @@ +# Tipps & Hinweise + +## Titelbild und Logo einfügen + +- Ein Logo macht das Spiel persönlicher! Die Funktion zum Laden eines Logos ist im Code vorbereitet, aber aktuell deaktiviert. +- Die Kids können später ein eigenes Logo als `images/logo.png` erstellen und die folgende Zeile in `water-game.py` aktivieren: + + ```python + # logo_img = pygame.image.load("images/logo.png") + # pygame.display.set_icon(logo_img) + ``` + +- Tipp: Das Logo sollte quadratisch und als PNG gespeichert sein. +- So kann jeder sein eigenes Spiel-Icon gestalten! + +## Titelbildschirm, Gewinn- und Verlustbildschirm + +- Implementiere einen Titelbildschirm und zeige bei Spielende einen Gewinn- oder Verlustbildschirm (siehe Codebeispiel unten). + +**Beispiel für Titelbildschirm und Gewinn/Verlust:** + +```python +# Titelbildschirm anzeigen +show_title_screen(WIN) +# ... +if win_condition: + show_win_screen(WIN) +elif lose_condition: + show_lose_screen(WIN) +``` + +**So kannst du Gewinn- und Verlustbildschirme umsetzen:** + +1. Schreibe eine Funktion, die den Bildschirm einfärbt und einen Text anzeigt: + +```python +def show_win_screen(win): + win.fill((40, 180, 80)) + font = pygame.font.SysFont(None, 72) + text = font.render("Gewonnen!", True, (255, 255, 255)) + win.blit(text, (win.get_width() // 2 - text.get_width() // 2, win.get_height() // 2 - text.get_height() // 2)) + pygame.display.update() + pygame.time.wait(2500) + +def show_lose_screen(win): + win.fill((180, 40, 40)) + font = pygame.font.SysFont(None, 72) + text = font.render("Verloren!", True, (255, 255, 255)) + win.blit(text, (win.get_width() // 2 - text.get_width() // 2, win.get_height() // 2 - text.get_height() // 2)) + pygame.display.update() + pygame.time.wait(2500) +``` + +2. Rufe die jeweilige Funktion auf, wenn die Gewinn- oder Verlustbedingung erfüllt ist: + +```python +if win_condition: + show_win_screen(WIN) +elif lose_condition: + show_lose_screen(WIN) +``` + +- Die Bedingungen kannst du selbst festlegen, z.B. alle Gegner besiegt = Gewinn, Spieler tot = Verlust. +- Nach dem Bildschirm kannst du das Spiel beenden oder neu starten. + +## Gameplay-Tipp: Begrenzte Wasserflaschen & Auffüllen an Trinkbrunnen + +- Implementiere eine Variable für die Anzahl der Wasserflaschen (Munition), z.B. `player.ammo`. +- Verringere `player.ammo` bei jedem Schuss (`SPACE`). +- Erlaube das Auffüllen der Munition, wenn der Spieler einen Trinkbrunnen-Pixel berührt (z.B. durch Kollisionsabfrage mit Trinkbrunnen-Positionen). +- Zeige die aktuelle Munition als Zahl oder Symbol im HUD an. + +**Beispiel (Pseudo-Code):** + +```python +# Beim Schießen: +if player.ammo > 0: + bottles.append(TapWater(player.x, player.y, player.dir)) + player.ammo -= 1 + +# Beim Berühren eines Trinkbrunnens: +for brunnen in trinkbrunnen_list: + if player.rect.colliderect(brunnen.rect): + player.ammo = MAX_AMMO +``` + +- Die Trinkbrunnen-Positionen kannst du aus der GeoJSON oder aus einer Liste von Pixelkoordinaten laden. +- So wird das Sammeln und Nachfüllen von Wasser spielerisch relevant! + +## .exe erstellen (Windows) + +**Kurzanleitung auf Deutsch:** + +1. Installiere PyInstaller: + + ```sh + pip install pyinstaller + ``` + +2. Konvertiere dein Logo zu einer `.ico`-Datei (z.B. mit [favicon.io](https://favicon.io/)). +3. Erstelle die .exe: + + ```sh + pyinstaller --onefile --windowed --icon=images/logo.ico water-game.py + ``` + +4. Die ausführbare Datei findest du im `dist`-Ordner. + +## Mac: App als ausführbare Datei erstellen + +**Kurzanleitung auf Deutsch:** + +1. Installiere PyInstaller: + + ```sh + pip install pyinstaller + ``` + +2. Erstelle die Mac-App: + + ```sh + pyinstaller --onefile --windowed --icon=images/logo.png water-game.py + ``` + + - Das Icon kann als PNG verwendet werden. + - Die App wird im `dist`-Ordner als ausführbare Datei erscheinen. + +3. (Optional) Um eine echte Mac-App zu erzeugen, nutze das Flag `--name` und `--osx-bundle-identifier`: + + ```sh + pyinstaller --onefile --windowed --icon=images/logo.png --name=WasserGame --osx-bundle-identifier=com.deinname.wassergame water-game.py + ``` + +**Hinweis:** + +- Die App kann per Doppelklick gestartet werden. +- Für die Verteilung an andere Macs kann eine Code-Signierung und Notarisierung nötig sein (siehe [Apple Doku](https://developer.apple.com/documentation/xcode/notarizing_macos_software_before_distribution)). + +## Tipp: Intro-Video vor Spielstart abspielen + +- Du kannst ein Video (z.B. MP4) als Intro vor dem eigentlichen Spiel abspielen. +- Nutze dazu z.B. die Bibliothek [`pygame-vlc`](https://github.com/pygame/pygame-vlc) oder [`opencv-python`](https://pypi.org/project/opencv-python/) zum Abspielen von Videos im Pygame-Fenster. + +**Beispiel mit pygame-vlc:** + +```python +import vlc +import pygame + +def play_intro_video(win, video_path): + instance = vlc.Instance() + player = instance.media_player_new() + media = instance.media_new(video_path) + player.set_media(media) + player.set_xwindow(win.get_window_id()) # Für Linux, für Windows/Mac ggf. anpassen + player.play() + # Warte bis das Video fertig ist oder eine Taste gedrückt wird + playing = True + while playing: + for event in pygame.event.get(): + if event.type == pygame.KEYDOWN or event.type == pygame.QUIT: + player.stop() + playing = False + pygame.time.wait(100) +``` + +- Das Video kann z.B. als `intro.mp4` im Projektordner liegen. +- Nach dem Intro kannst du wie gewohnt den Titelbildschirm anzeigen. +- Für Windows/Mac kann die Fenster-ID Methode abweichen, siehe Doku von pygame-vlc. + +## Zusätzliche Hinweise + +- Stelle sicher, dass alle Bilder und Daten im richtigen Pfad liegen. +- Für komplexe Projekte nutze eine [PyInstaller spec file](https://pyinstaller.org/en/stable/spec-files.html). diff --git a/align_trinkbrunnen.py b/align_trinkbrunnen.py new file mode 100644 index 0000000..9e7a526 --- /dev/null +++ b/align_trinkbrunnen.py @@ -0,0 +1,48 @@ +import rasterio +import geopandas as gpd +import numpy as np +import matplotlib.pyplot as plt + +# --- CONFIG --- +raster_path = 'geodata/s2_2025.tif' +geojson_path = 'geodata/Trinkbrunnen_Berlin.geojson' + +# --- LOAD RASTER --- +with rasterio.open(raster_path) as src: + map_img = src.read([1, 2, 3]) + map_img = np.transpose(map_img, (1, 2, 0)) + map_transform = src.transform + map_crs = src.crs + map_bounds = src.bounds + map_width = src.width + map_height = src.height + +# --- LOAD GEOJSON --- +gdf = gpd.read_file(geojson_path) +if gdf.crs != map_crs: + gdf = gdf.to_crs(map_crs) + +# --- PROJECT POINTS TO PIXELS --- +pixel_positions = [] +for geom in gdf.geometry: + if geom and geom.type == 'Point': + x, y = geom.x, geom.y + px, py = ~map_transform * (x, y) + pixel_positions.append((int(px), int(py))) + +# --- VISUALIZE --- + +plt.figure(figsize=(10, 10)) +plt.imshow(map_img) +for px, py in pixel_positions: + plt.scatter(px, py, c='deepskyblue', s=40, edgecolors='black', label='Trinkbrunnen') +plt.title('Trinkbrunnen auf der Berlin-Karte') +plt.axis('off') +plt.savefig('datenvisualisierung/trinkbrunnen_karte.png', bbox_inches='tight', dpi=200) +plt.show() + +# --- EXPORT PIXEL POSITIONS FOR GAME --- +# Save as npy for easy loading in pygame +np.save('trinkbrunnen_pixel_positions.npy', np.array(pixel_positions)) + +print(f"{len(pixel_positions)} Trinkbrunnen-Pixelpositionen wurden in trinkbrunnen_pixel_positions.npy gespeichert.") diff --git a/check_tif.py b/check_tif.py new file mode 100644 index 0000000..46de0d7 --- /dev/null +++ b/check_tif.py @@ -0,0 +1,25 @@ +import rasterio +import numpy as np +import matplotlib.pyplot as plt +import sys + +# --- Konfiguration --- +tif_path = 'geodata/s2_2025.tif' + +try: + with rasterio.open(tif_path) as src: + print(f"Datei: {tif_path}") + print(f"CRS: {src.crs}") + print(f"Breite x Höhe: {src.width} x {src.height}") + print(f"Bänder: {src.count}") + print(f"Datentyp: {src.dtypes}") + arr = src.read(1) + print(f"Min: {np.nanmin(arr)}, Max: {np.nanmax(arr)}") + plt.imshow(arr, cmap='gray') + plt.title('Erstes Band der TIFF-Datei') + plt.axis('off') + plt.savefig('datenvisualisierung/tif_check.png', bbox_inches='tight', dpi=200) + plt.show() +except Exception as e: + print(f"Fehler beim Laden der TIFF-Datei: {e}") + sys.exit(1) diff --git a/enemy.py b/enemy.py new file mode 100644 index 0000000..08139ce --- /dev/null +++ b/enemy.py @@ -0,0 +1,94 @@ +import pygame +import random +from settings import PLAYER_SIZE, PLAYER_SPEED, KILL_RADIUS + +class Enemy: + def __init__(self, x, y, enemy_image, human_image, controlled=False): + self.x = x + self.y = y + self.rect = pygame.Rect(x, y, PLAYER_SIZE, PLAYER_SIZE) + self.dir = "up" + self.max_health = 100 + self.health = 100 + self.is_human = False + self.enemy_image = enemy_image + self.human_image = human_image + self.controlled = controlled + self.alive = True + + def move(self, keys, player, map_w, map_h): + if self.is_human: + if random.random() < 0.02: + self.dir = random.choice(["up", "down", "left", "right"]) + dx = dy = 0 + if self.dir == "up": + dy -= PLAYER_SPEED // 1.5 + if self.dir == "down": + dy += PLAYER_SPEED // 2 + if self.dir == "left": + dx -= PLAYER_SPEED // 2 + if self.dir == "right": + dx += PLAYER_SPEED // 2 + self.x = max(0, min(map_w - PLAYER_SIZE // 2, self.x + dx)) + self.y = max(0, min(map_h - PLAYER_SIZE // 2, self.y + dy)) + self.rect.topleft = (self.x, self.y) + elif self.controlled: + dx = dy = 0 + if keys[pygame.K_UP]: + dy -= PLAYER_SPEED // 1.5 + self.dir = "up" + if keys[pygame.K_DOWN]: + dy += PLAYER_SPEED // 1.5 + self.dir = "down" + if keys[pygame.K_LEFT]: + dx -= PLAYER_SPEED // 1.5 + self.dir = "left" + if keys[pygame.K_RIGHT]: + dx += PLAYER_SPEED // 1.5 + self.dir = "right" + self.x = max(0, min(map_w - PLAYER_SIZE // 2, self.x + dx)) + self.y = max(0, min(map_h - PLAYER_SIZE // 2, self.y + dy)) + self.rect.topleft = (self.x, self.y) + else: + if player and player.alive: + dx = player.x - self.x + dy = player.y - self.y + dist = max(1, (dx ** 2 + dy ** 2) ** 0.5) + speed = PLAYER_SPEED // 2 + self.x += int(speed * dx / dist) + self.y += int(speed * dy / dist) + self.x = max(0, min(map_w - PLAYER_SIZE // 2, self.x)) + self.y = max(0, min(map_h - PLAYER_SIZE // 2, self.y)) + self.rect.topleft = (self.x, self.y) + + def draw(self, win, ox, oy): + img = self.human_image if self.is_human else self.enemy_image + angle = {"up": 90, "right": 0, "down": -90, "left": 180}[self.dir] + color = (0, 128, 255) if self.is_human else (200, 200, 0) + if img: + rotated_img = pygame.transform.rotate(img, angle) + win.blit(rotated_img, (self.x - ox, self.y - oy)) + else: + pygame.draw.rect(win, color, (self.x - ox, self.y - oy, PLAYER_SIZE // 2, PLAYER_SIZE // 2)) + if not self.is_human: + bw = PLAYER_SIZE // 2 + bh = 6 + ratio = self.health / self.max_health + bx = self.x - ox + by = self.y - oy - bh - 2 + pygame.draw.rect(win, (255, 0, 0), (bx, by, bw, bh)) + pygame.draw.rect(win, (0, 255, 0), (bx, by, int(bw * ratio), bh)) + + def take_damage(self, amt): + if not self.is_human: + self.health = max(0, self.health - amt) + if self.health == 0: + self.alive = False + self.is_human = True + + def check_kill_player(self, player): + if not self.is_human: + dx = (self.x + PLAYER_SIZE // 4) - (player.x + PLAYER_SIZE // 2) + dy = (self.y + PLAYER_SIZE // 4) - (player.y + PLAYER_SIZE // 2) + if (dx ** 2 + dy ** 2) ** 0.5 < KILL_RADIUS: + player.alive = False diff --git a/player.py b/player.py new file mode 100644 index 0000000..fefa8a1 --- /dev/null +++ b/player.py @@ -0,0 +1,42 @@ +import pygame +from settings import PLAYER_SIZE + +class Player: + def __init__(self, x, y, image): + self.x = x + self.y = y + self.alive = True + self.rect = pygame.Rect(x, y, PLAYER_SIZE, PLAYER_SIZE) + self.dir = "up" + self.image = image + + def move(self, keys, map_w, map_h): + if not self.alive: + return + dx = dy = 0 + if keys[pygame.K_w]: + dy -= 5 + self.dir = "up" + if keys[pygame.K_s]: + dy += 5 + self.dir = "down" + if keys[pygame.K_a]: + dx -= 5 + self.dir = "left" + if keys[pygame.K_d]: + dx += 5 + self.dir = "right" + self.x = max(0, min(map_w - PLAYER_SIZE, self.x + dx)) + self.y = max(0, min(map_h - PLAYER_SIZE, self.y + dy)) + self.rect.topleft = (self.x, self.y) + + def draw(self, win, ox, oy): + if not self.alive: + return + img = self.image + angle = {"up": 90, "right": 0, "down": -90, "left": 180}[self.dir] + if img: + rotated_img = pygame.transform.rotate(img, angle) + win.blit(rotated_img, (self.x - ox, self.y - oy)) + else: + pygame.draw.rect(win, (200, 200, 0), (self.x - ox, self.y - oy, PLAYER_SIZE, PLAYER_SIZE)) diff --git a/results/lst.tif b/results/lst.tif deleted file mode 100644 index 26d265f..0000000 Binary files a/results/lst.tif and /dev/null differ diff --git a/results/s2_2025.tif b/results/s2_2025.tif deleted file mode 100644 index 59c6980..0000000 Binary files a/results/s2_2025.tif and /dev/null differ diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..11413a2 --- /dev/null +++ b/settings.py @@ -0,0 +1,26 @@ +import pygame + +# Globale Einstellungen für das Spiel +MAP_PATH = "geodata/s2_2025.tif" +WIDTH = 800 +HEIGHT = 600 +MINIMAP_MARGIN = 20 + +PLAYER_SIZE = 40 +PLAYER_SPEED = 5 +WATER_SIZE = 10 +WATER_SPEED = 10 +KILL_RADIUS = 30 + +PLAYER_IMG_PATH = "images/player.png" +ENEMY_IMG_PATH = "images/enemy.png" +HUMAN_IMG_PATH = "images/human.png" + + +def load_img(path, size): + try: + img = pygame.image.load(path).convert_alpha() + return pygame.transform.scale(img, size) + except Exception as e: + print(f"Error loading image '{path}': {e}") + return None diff --git a/tapwater.py b/tapwater.py new file mode 100644 index 0000000..1f37a61 --- /dev/null +++ b/tapwater.py @@ -0,0 +1,23 @@ +import pygame +from settings import PLAYER_SIZE, WATER_SIZE, WATER_SPEED + +class TapWater: + def __init__(self, x, y, direction): + self.x = x + PLAYER_SIZE // 2 - WATER_SIZE // 2 + self.y = y + PLAYER_SIZE // 2 - WATER_SIZE // 2 + self.direction = direction + self.rect = pygame.Rect(self.x, self.y, WATER_SIZE, WATER_SIZE) + + def move(self): + if self.direction == "up": + self.y -= WATER_SPEED + elif self.direction == "down": + self.y += WATER_SPEED + elif self.direction == "left": + self.x -= WATER_SPEED + elif self.direction == "right": + self.x += WATER_SPEED + self.rect.topleft = (self.x, self.y) + + def draw(self, win, ox, oy): + pygame.draw.rect(win, (0, 180, 255), (self.x - ox, self.y - oy, WATER_SIZE, WATER_SIZE)) diff --git a/water-game.py b/water-game.py index 5b592d4..fbcd969 100644 --- a/water-game.py +++ b/water-game.py @@ -1,32 +1,22 @@ -# Simple top-down shooter with WASD player, arrow-key enemy, health bar, minimap, and raster background -import pygame, sys, numpy as np, rasterio, random +import sys +import random +import pygame +import numpy as np +import rasterio + +from player import Player +from enemy import Enemy +from tapwater import TapWater +from settings import PLAYER_SIZE, MAP_PATH, WIDTH, HEIGHT, MINIMAP_MARGIN + +DEBUG = False pygame.init() -WIDTH, HEIGHT = 800, 600 +# logo_img = pygame.image.load("images/logo.png") +# pygame.display.set_icon(logo_img) WIN = pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption("Water Bottle Shooter") -PLAYER_SIZE, PLAYER_SPEED = 40, 5 -WATER_SIZE, WATER_SPEED = 10, 10 -KILL_RADIUS = 30 -MINIMAP_SIZE, MINIMAP_MARGIN = 200, 20 -PLAYER_IMG_PATH = "images/player.png" -ENEMY_IMG_PATH = "images/enemy.png" -HUMAN_IMG_PATH = "images/human.png" -MAP_PATH = "results/s2_2025.tif" - -# Load images -def load_img(path, size): - try: - img = pygame.image.load(path).convert_alpha() - return pygame.transform.scale(img, size) - except: - return None -PLAYER_IMAGE = load_img(PLAYER_IMG_PATH, (PLAYER_SIZE, PLAYER_SIZE)) -ENEMY_IMAGE = load_img(ENEMY_IMG_PATH, (PLAYER_SIZE, PLAYER_SIZE)) - -HUMAN_IMAGE = load_img(HUMAN_IMG_PATH, (PLAYER_SIZE, PLAYER_SIZE)) -# Load raster map def load_map(path): try: arr = rasterio.open(path).read(1) @@ -34,156 +24,41 @@ def load_map(path): arr = np.clip(arr, np.percentile(arr, 5), np.percentile(arr, 95)) arr = ((arr - arr.min()) / (arr.max() - arr.min()) * 255).astype(np.uint8) arr = np.stack([arr, arr, arr], -1) - arr = np.rot90(arr, 3) - arr = np.fliplr(arr) surf = pygame.surfarray.make_surface(arr) return surf, arr.shape[1], arr.shape[0] except Exception as e: - print(f"Map load error: {e}"); return None, WIDTH, HEIGHT + print(f"Map load error: {e}") + return None, WIDTH, HEIGHT + map_surface, MAP_W, MAP_H = load_map(MAP_PATH) - -class Player: - def __init__(self, x, y): - self.x, self.y, self.alive = x, y, True - self.rect = pygame.Rect(x, y, PLAYER_SIZE, PLAYER_SIZE) - self.dir = "up" - def move(self, keys): - if not self.alive: return - dx = dy = 0 - if keys[pygame.K_w]: dy -= PLAYER_SPEED; self.dir = "up" - if keys[pygame.K_s]: dy += PLAYER_SPEED; self.dir = "down" - if keys[pygame.K_a]: dx -= PLAYER_SPEED; self.dir = "left" - if keys[pygame.K_d]: dx += PLAYER_SPEED; self.dir = "right" - self.x = max(0, min(MAP_W-PLAYER_SIZE, self.x+dx)) - self.y = max(0, min(MAP_H-PLAYER_SIZE, self.y+dy)) - self.rect.topleft = (self.x, self.y) - - def draw(self, win, ox, oy): - if not self.alive: return - img = PLAYER_IMAGE or None - angle = {"up": 90, "right": 0, "down": -90, "left": 180}[self.dir] - - if img: - rotated_img = pygame.transform.rotate(img, angle) - win.blit(rotated_img, (self.x-ox, self.y-oy)) - else: pygame.draw.rect(win, (200,200,0), (self.x-ox, self.y-oy, PLAYER_SIZE, PLAYER_SIZE)) - -class Enemy: - def __init__(self, x, y,controlled=False): - self.x, self.y = x, y - self.rect = pygame.Rect(x, y, PLAYER_SIZE, PLAYER_SIZE) - self.dir = "up" - self.max_health, self.health = 100, 100 - self.is_human = False - self.controlled = controlled - - - - def move(self, keys, player=None): - if self.is_human: - # Move randomly - if random.random() < 0.02: - self.dir = random.choice(["up", "down", "left", "right"]) - dx = dy = 0 - if self.dir == "up": dy -= PLAYER_SPEED//1.5 - if self.dir == "down": dy += PLAYER_SPEED//2 - if self.dir == "left": dx -= PLAYER_SPEED//2 - if self.dir == "right": dx += PLAYER_SPEED//2 - self.x = max(0, min(MAP_W-PLAYER_SIZE//2, self.x+dx)) - self.y = max(0, min(MAP_H-PLAYER_SIZE//2, self.y+dy)) - self.rect.topleft = (self.x, self.y) - elif self.controlled: - dx = dy = 0 - if keys[pygame.K_UP]: dy -= PLAYER_SPEED//1.5; self.dir = "up" - if keys[pygame.K_DOWN]: dy += PLAYER_SPEED//1.5; self.dir = "down" - if keys[pygame.K_LEFT]: dx -= PLAYER_SPEED//1.5; self.dir = "left" - if keys[pygame.K_RIGHT]: dx += PLAYER_SPEED//1.5; self.dir = "right" - self.x = max(0, min(MAP_W-PLAYER_SIZE//2, self.x+dx)) - self.y = max(0, min(MAP_H-PLAYER_SIZE//2, self.y+dy)) - self.rect.topleft = (self.x, self.y) - else: - # Chase the player - if player and player.alive: - dx = player.x - self.x - dy = player.y - self.y - dist = max(1, (dx**2 + dy**2)**0.5) - speed = PLAYER_SPEED//2 - self.x += int(speed * dx / dist) - self.y += int(speed * dy / dist) - self.x = max(0, min(MAP_W-PLAYER_SIZE//2, self.x)) - self.y = max(0, min(MAP_H-PLAYER_SIZE//2, self.y)) - self.rect.topleft = (self.x, self.y) - - - def draw(self, win, ox, oy): - enemy_img = ENEMY_IMAGE or None - human_img = HUMAN_IMAGE or None - color = (0, 128, 255) if self.is_human else (200, 200, 0) - if enemy_img and not self.is_human: - win.blit(enemy_img, (self.x-ox, self.y-oy)) - elif human_img and self.is_human: - win.blit(human_img, (self.x-ox, self.y-oy)) - else: - pygame.draw.rect(win, color, (self.x-ox, self.y-oy, PLAYER_SIZE//2, PLAYER_SIZE//2)) - # Health bar (only for zombie) - if not self.is_human: - bw, bh = PLAYER_SIZE//2, 6 - ratio = self.health/self.max_health - bx, by = self.x-ox, self.y-oy-bh-2 - pygame.draw.rect(win, (255,0,0), (bx,by,bw,bh)) - pygame.draw.rect(win, (0,255,0), (bx,by,int(bw*ratio),bh)) - - def take_damage(self, amt): - if not self.is_human: - self.health = max(0, self.health-amt) - if self.health == 0: - self.is_human = True - - def check_kill_player(self, player): - if not self.is_human: - dx = (self.x+PLAYER_SIZE//4)-(player.x+PLAYER_SIZE//2) - dy = (self.y+PLAYER_SIZE//4)-(player.y+PLAYER_SIZE//2) - if (dx**2+dy**2)**0.5 < KILL_RADIUS: player.alive = False - -class TapWater: - def __init__(self, x, y, dir): - self.x = x+PLAYER_SIZE//2-WATER_SIZE//2 - self.y = y+PLAYER_SIZE//2-WATER_SIZE//2 - self.dir = dir - self.rect = pygame.Rect(self.x, self.y, WATER_SIZE, WATER_SIZE) - def move(self): - if self.dir=="up": self.y -= WATER_SPEED - elif self.dir=="down": self.y += WATER_SPEED - elif self.dir=="left": self.x -= WATER_SPEED - elif self.dir=="right": self.x += WATER_SPEED - self.rect.topleft = (self.x, self.y) - def draw(self, win, ox, oy): - pygame.draw.rect(win, (0,180,255), (self.x-ox, self.y-oy, WATER_SIZE, WATER_SIZE)) +MINIMAP_WIDTH = 200 +MINIMAP_HEIGHT = int(MAP_H / MAP_W * MINIMAP_WIDTH) def draw_minimap(win, map_surface, player, enemies, bottles): - mini = pygame.transform.smoothscale(map_surface, (MINIMAP_SIZE, MINIMAP_SIZE)) - win.blit(mini, (WIDTH-MINIMAP_SIZE-MINIMAP_MARGIN, MINIMAP_MARGIN)) - def map2mini(x,y): - mx = int(x/MAP_W*MINIMAP_SIZE) - my = int(y/MAP_H*MINIMAP_SIZE) - return WIDTH-MINIMAP_SIZE-MINIMAP_MARGIN+mx, MINIMAP_MARGIN+my - pygame.draw.circle(win, (0,255,0), map2mini(player.x+PLAYER_SIZE//2, player.y+PLAYER_SIZE//2), 5) + mini = pygame.transform.smoothscale(map_surface, (MINIMAP_WIDTH, MINIMAP_HEIGHT)) + win.blit(mini, (WIDTH - MINIMAP_WIDTH - MINIMAP_MARGIN, MINIMAP_MARGIN)) + + def map2mini(x, y): + mx = int(x / MAP_W * MINIMAP_WIDTH) + my = int(y / MAP_H * MINIMAP_HEIGHT) + return WIDTH - MINIMAP_WIDTH - MINIMAP_MARGIN + mx, MINIMAP_MARGIN + my + + pygame.draw.circle(win, (0, 255, 0), map2mini(player.x + PLAYER_SIZE // 2, player.y + PLAYER_SIZE // 2), 5) for enemy in enemies: - color = (0,128,255) if enemy.is_human else (255,0,0) if enemy.controlled else (200,200,0) - ex, ey = map2mini(enemy.x+PLAYER_SIZE//4, enemy.y+PLAYER_SIZE//4) + color = (0, 128, 255) if enemy.is_human else (255, 0, 0) if enemy.controlled else (200, 200, 0) + ex, ey = map2mini(enemy.x + PLAYER_SIZE // 4, enemy.y + PLAYER_SIZE // 4) pygame.draw.circle(win, color, (ex, ey), 5) - for b in bottles: - pygame.draw.circle(win, (0,180,255), map2mini(b.x, b.y), 3) + for bottle in bottles: + pygame.draw.circle(win, (0, 180, 255), map2mini(bottle.x, bottle.y), 3) def main(): - player = Player(MAP_W//2, MAP_H//2) - # Spawn 20 zombies, only the first is controlled - enemies = [] - for i in range(20): - x = random.randint(0, MAP_W-PLAYER_SIZE) - y = random.randint(0, MAP_H-PLAYER_SIZE) - controlled = (i == 0) - enemies.append(Enemy(x, y, controlled=controlled)) + from settings import PLAYER_IMG_PATH, ENEMY_IMG_PATH, HUMAN_IMG_PATH, PLAYER_SIZE, load_img + player_img = load_img(PLAYER_IMG_PATH, (PLAYER_SIZE, PLAYER_SIZE)) + enemy_img = load_img(ENEMY_IMG_PATH, (PLAYER_SIZE, PLAYER_SIZE)) + human_img = load_img(HUMAN_IMG_PATH, (PLAYER_SIZE, PLAYER_SIZE)) + + player = Player(MAP_W // 2, MAP_H // 2, player_img) + enemies = [Enemy(random.randint(0, MAP_W - PLAYER_SIZE), random.randint(0, MAP_H - PLAYER_SIZE), enemy_img, human_img, i == 0) for i in range(20)] bottles = [] minimap_visible = True clock = pygame.time.Clock() @@ -191,35 +66,48 @@ def main(): while running: clock.tick(60) for event in pygame.event.get(): - if event.type==pygame.QUIT: running=False - if event.type==pygame.KEYDOWN: - if event.key==pygame.K_SPACE: bottles.append(TapWater(player.x, player.y, player.dir)) - if event.key==pygame.K_m: minimap_visible = not minimap_visible + if event.type == pygame.QUIT: + running = False + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_SPACE: + bottles.append(TapWater(player.x, player.y, player.dir)) + if event.key == pygame.K_m: + minimap_visible = not minimap_visible keys = pygame.key.get_pressed() - player.move(keys) + player.move(keys, MAP_W, MAP_H) for enemy in enemies: - enemy.move(keys, player) + enemy.move(keys, player, MAP_W, MAP_H) enemy.check_kill_player(player) - for b in bottles[:]: + for bottle in bottles[:]: for enemy in enemies: - if enemy.rect.colliderect(b.rect): enemy.take_damage(20); bottles.remove(b); break - bottles = [b for b in bottles if 0<=b.x<=MAP_W and 0<=b.y<=MAP_H] - ox = max(0, min(player.x+PLAYER_SIZE//2-WIDTH//2, MAP_W-WIDTH)) - oy = max(0, min(player.y+PLAYER_SIZE//2-HEIGHT//2, MAP_H-HEIGHT)) - if map_surface: WIN.blit(map_surface, (0,0), area=pygame.Rect(ox,oy,WIDTH,HEIGHT)) - else: WIN.fill((255,255,255)) + if enemy.rect.colliderect(bottle.rect): + enemy.take_damage(20) + bottles.remove(bottle) + break + bottles = [b for b in bottles if 0 <= b.x <= MAP_W and 0 <= b.y <= MAP_H] + ox = max(0, min(player.x + PLAYER_SIZE // 2 - WIDTH // 2, MAP_W - WIDTH)) + oy = max(0, min(player.y + PLAYER_SIZE // 2 - HEIGHT // 2, MAP_H - HEIGHT)) + if map_surface: + WIN.blit(map_surface, (0, 0), area=pygame.Rect(ox, oy, WIDTH, HEIGHT)) + else: + WIN.fill((255, 255, 255)) player.draw(WIN, ox, oy) - for b in bottles: b.draw(WIN, ox, oy) - for enemy in enemies: enemy.draw(WIN, ox, oy) + for bottle in bottles: + bottle.draw(WIN, ox, oy) + for enemy in enemies: + enemy.draw(WIN, ox, oy) if minimap_visible: - # Draw all enemies on minimap draw_minimap(WIN, map_surface, player, enemies, bottles) pygame.display.update() - if not player.alive: - font = pygame.font.SysFont(None, 48) - text = font.render("Game Over!", True, (255,0,0)) - WIN.blit(text, (WIDTH//2-text.get_width()//2, HEIGHT//2-text.get_height()//2)) - pygame.display.update(); pygame.time.wait(2000); running=False - pygame.quit(); sys.exit() + # Remove win/lose condition checks + # if not player.alive: + # show_lose_screen(WIN) + # running = False + # elif all(not e.alive or e.health <= 0 for e in enemies): + # show_win_screen(WIN) + # running = False + pygame.quit() + sys.exit() -if __name__=="__main__": main() +if __name__ == "__main__": + main()