add tip list
This commit is contained in:
parent
7b9d7734ab
commit
c733cf64e1
12 changed files with 598 additions and 187 deletions
61
.gitignore
vendored
Normal file
61
.gitignore
vendored
Normal file
|
|
@ -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
|
||||
30
README.md
30
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
|
||||
174
README_TIPS.md
Normal file
174
README_TIPS.md
Normal file
|
|
@ -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).
|
||||
48
align_trinkbrunnen.py
Normal file
48
align_trinkbrunnen.py
Normal file
|
|
@ -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.")
|
||||
25
check_tif.py
Normal file
25
check_tif.py
Normal file
|
|
@ -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)
|
||||
94
enemy.py
Normal file
94
enemy.py
Normal file
|
|
@ -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
|
||||
42
player.py
Normal file
42
player.py
Normal file
|
|
@ -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))
|
||||
BIN
results/lst.tif
BIN
results/lst.tif
Binary file not shown.
Binary file not shown.
26
settings.py
Normal file
26
settings.py
Normal file
|
|
@ -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
|
||||
23
tapwater.py
Normal file
23
tapwater.py
Normal file
|
|
@ -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))
|
||||
262
water-game.py
262
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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue