Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
486fea38e7 | ||
|
|
9421d8d47c | ||
|
|
509b3433b8 | ||
|
|
bbafc3bbba | ||
|
|
b243cf04d3 | ||
|
|
eaafd92dc2 | ||
|
|
b60ffd87aa | ||
|
|
02202e4d3d | ||
|
|
e7c5ebb119 | ||
|
|
9a86a3734f |
@@ -0,0 +1,107 @@
|
||||
---
|
||||
applyTo: "tools/vernon/**,assets/Rat/**"
|
||||
---
|
||||
|
||||
# Pixel Art Sprite Workflow — mice project
|
||||
|
||||
## Strumenti disponibili
|
||||
|
||||
| Script | Uso |
|
||||
|--------|-----|
|
||||
| `tools/vernon/image_to_json.py <INPUT.png> <OUTPUT.json>` | Converte PNG → matrice JSON RGBA 64×64 |
|
||||
| `tools/vernon/json_to_png.py <INPUT.json> <OUTPUT.png>` | Converte matrice JSON RGBA → PNG |
|
||||
|
||||
Entrambi usano Pillow e richiedono il `venv` attivo:
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
## Formato JSON
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "BMP_BOMB0.png",
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"mode": "RGBA",
|
||||
"pixels": [
|
||||
[ [R, G, B, A], ... ], // riga 0, 64 pixel
|
||||
... // 64 righe totali
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Ogni pixel è `[R, G, B, A]` con valori 0–255.
|
||||
|
||||
## Convenzioni cromatiche del gioco
|
||||
|
||||
- **Colore trasparente (chromakey):** `[128, 128, 128, 192]` — usato come sfondo, il motore lo rende hidden
|
||||
- **Alpha standard:** `192` per tutti i pixel visibili (coerente con gli asset originali)
|
||||
|
||||
## Workflow iterativo di redesign (passi 0–4)
|
||||
|
||||
```
|
||||
0. BACKUP → prima di sovrascrivere, copia l'originale:
|
||||
cp assets/Rat/<NAME>.png assets/Rat/backup/<NAME>_original.png
|
||||
1. image_to_json.py → esamina JSON e PNG originale
|
||||
2. capire struttura: sfondo, palette, forma principale
|
||||
3. modificare JSON (o generarlo via script Python) con:
|
||||
- più livelli di shading (8+ valori invece di 3)
|
||||
- dettagli geometrici aggiuntivi (texture, bordi, ombre interne)
|
||||
- palette più ricca mantenendo stile pixel art (bordi netti, no anti-alias)
|
||||
4. json_to_png.py → valuta risultato visivo; se non soddisfacente, torna a 3
|
||||
```
|
||||
|
||||
## Pattern Python per generare JSON programmaticamente
|
||||
|
||||
```python
|
||||
import json, math
|
||||
from pathlib import Path
|
||||
|
||||
W, H = 64, 64
|
||||
A = 192 # alpha standard
|
||||
|
||||
def px(r, g, b): return [r, g, b, A]
|
||||
|
||||
TRANSPARENT = px(128, 128, 128)
|
||||
grid = [[TRANSPARENT[:] for _ in range(W)] for _ in range(H)]
|
||||
|
||||
def put(x, y, col):
|
||||
if 0 <= x < W and 0 <= y < H:
|
||||
grid[y][x] = col[:]
|
||||
|
||||
# ... disegna su grid ...
|
||||
|
||||
data = {"source": "BMP_X.png", "width": W, "height": H, "mode": "RGBA", "pixels": grid}
|
||||
Path("tools/vernon/output/BMP_X_v2.json").write_text(json.dumps(data, indent=2))
|
||||
```
|
||||
|
||||
## Tecniche pixel art a 64×64
|
||||
|
||||
- **Shading sferico:** calcola normale + dot product con luce per N livelli di grigio discreti
|
||||
- **Rope/miccia:** traccia bezier quadratica, alterna 2–3 toni in sequenza (effetto intrecciato)
|
||||
- **Scintilla:** pixel centrali chiari (bianco/giallo), bordi che degradano in arancio → rosso
|
||||
- **Outline:** bordo di 1px nero (`[0,0,0,192]`) attorno a tutte le forme principali
|
||||
- **Nessun anti-aliasing:** ogni pixel è un colore solido discreto della palette scelta
|
||||
|
||||
## Asset da redesignare (tutti 64×64)
|
||||
|
||||
| File | Gruppo |
|
||||
|------|--------|
|
||||
| `BMP_BOMB0.png` … `BMP_BOMB4.png` | Animazione bomba (0=quieta, 4=accesa) |
|
||||
| `BMP_1_GRASS_1.png` … `BMP_1_GRASS_4.png` | Tile erba tema 1 (verde) — **redesignate con FBM 7-toni** |
|
||||
| `BMP_2_GRASS_1.png` … `BMP_2_GRASS_4.png` | Tile erba tema 2 (secca/autunnale) |
|
||||
| `BMP_3_GRASS_1.png` … `BMP_3_GRASS_4.png` | Tile erba tema 3 (dungeon/pietra) |
|
||||
| `BMP_4_GRASS_1.png` … `BMP_4_GRASS_4.png` | Tile erba tema 4 (fuoco/lava) |
|
||||
| `BMP_GAS.png`, `BMP_GAS_{DIR}.png` | Gas generico + 4 direzioni |
|
||||
| `BMP_EXPLOSION.png`, `BMP_EXPLOSION_{DIR}.png` | Esplosione generica + 4 direzioni |
|
||||
| `BMP_NUCLEAR.png` | Fungo nucleare |
|
||||
| `BMP_POISON.png` | Veleno |
|
||||
|
||||
## Note sull'animazione BOMB (frame 0–4)
|
||||
|
||||
- `BOMB0`: bomba ferma, scintilla piccola a riposo
|
||||
- `BOMB1`–`BOMB3`: miccia che brucia (la scintilla avanza verso il corpo, la corda si accorcia)
|
||||
- `BOMB4`: quasi esplode (glow rosso/arancio sul corpo, scintilla grande)
|
||||
|
||||
Per i frame animati: mantieni identici corpo + miccia, varia solo posizione/dimensione scintilla e eventuale glow progressivo.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Piano di distribuzione ARM con AppImage
|
||||
|
||||
Questo repository ora e pronto per essere portato dentro un bundle AppImage senza dipendere dalla directory corrente e senza scrivere nel filesystem montato in sola lettura dell'AppImage.
|
||||
|
||||
## Stato attuale
|
||||
|
||||
- Le risorse di runtime vengono risolte a partire dal root del progetto tramite `MICE_PROJECT_ROOT`.
|
||||
- I dati persistenti (`scores.txt`, `user_profiles.json`) vengono scritti in una directory utente persistente:
|
||||
- `MICE_DATA_DIR`, se impostata.
|
||||
- altrimenti `${XDG_DATA_HOME}/mice`.
|
||||
- fallback: `~/.local/share/mice`.
|
||||
- E presente uno scaffold di packaging in `packaging/`.
|
||||
|
||||
## Strategia consigliata
|
||||
|
||||
1. Costruire l'AppImage su una macchina `aarch64` reale o in una chroot/container ARM.
|
||||
2. Creare dentro `AppDir` un ambiente Python copiato localmente con `python -m venv --copies`.
|
||||
3. Installare le dipendenze Python da `requirements.txt` dentro quel Python locale.
|
||||
4. Copiare il gioco e gli asset in `AppDir/usr/share/mice`.
|
||||
5. Bundlare le librerie native richieste da SDL2 e dai wheel Python dentro `AppDir/usr/lib`.
|
||||
6. Usare `AppRun` per esportare `LD_LIBRARY_PATH`, `MICE_PROJECT_ROOT` e `MICE_DATA_DIR` prima del lancio di `rats.py`.
|
||||
7. Generare il file finale con `appimagetool`.
|
||||
|
||||
## Perche costruire nativamente su ARM
|
||||
|
||||
- Un AppImage deve contenere binari della stessa architettura del target.
|
||||
- `PySDL2`, `numpy` e `Pillow` portano con se librerie native o dipendenze native.
|
||||
- Il cross-build da `x86_64` a `aarch64` e possibile, ma aumenta molto il rischio di incompatibilita su `glibc`, `libSDL2` e wheel Python.
|
||||
|
||||
## Comando di build
|
||||
|
||||
Da una macchina Linux `aarch64` con `python3`, `rsync`, `ldd`, `ldconfig` e `appimagetool` disponibili:
|
||||
|
||||
```bash
|
||||
./packaging/build_appimage_aarch64.sh
|
||||
```
|
||||
|
||||
Output previsto:
|
||||
|
||||
- `dist/AppDir`
|
||||
- `dist/Mice-aarch64.AppImage`
|
||||
|
||||
## Dipendenze host richieste al builder ARM
|
||||
|
||||
Serve un sistema di build ARM con almeno:
|
||||
|
||||
- `python3`
|
||||
- `python3-venv`
|
||||
- `rsync`
|
||||
- `glibc` userland standard
|
||||
- `appimagetool`
|
||||
- librerie di sviluppo/runtime installate sul builder, in particolare:
|
||||
- `libSDL2`
|
||||
- `libSDL2_ttf`
|
||||
|
||||
## Test minimi da fare sul target ARM
|
||||
|
||||
1. Avvio del gioco da shell.
|
||||
2. Caricamento font e immagini.
|
||||
3. Riproduzione audio WAV.
|
||||
4. Salvataggio punteggi in `~/.local/share/mice/scores.txt`.
|
||||
5. Creazione e lettura profili in `~/.local/share/mice/user_profiles.json`.
|
||||
6. Cambio livello da `assets/Rat/level.dat`.
|
||||
|
||||
## Rischi residui
|
||||
|
||||
- La relocazione di un venv copiato dentro AppImage e pratica, ma va verificata sul target reale.
|
||||
- Se il target ARM ha un userland molto vecchio, conviene costruire l'AppImage su una distro ARM con `glibc` piu vecchia del target.
|
||||
- Se emergono problemi di relocazione del Python del venv, il passo successivo corretto e passare a un Python relocatable tipo `python-build-standalone` mantenendo invariato il launcher.
|
||||
|
||||
## File introdotti
|
||||
|
||||
- `runtime_paths.py`
|
||||
- `packaging/appimage/AppRun`
|
||||
- `packaging/appimage/mice.desktop`
|
||||
- `packaging/build_appimage_aarch64.sh`
|
||||
@@ -0,0 +1,773 @@
|
||||
# NumPy Tutorial: Dal Tuo Sistema di Collisioni al Codice Ottimizzato
|
||||
|
||||
Questo documento spiega NumPy usando come esempio reale il sistema di collisioni di Mice!, confrontando il tuo approccio originale con la versione ottimizzata.
|
||||
|
||||
## Indice
|
||||
1. [Introduzione: Il Problema delle Performance](#1-introduzione-il-problema-delle-performance)
|
||||
2. [Cos'è NumPy e Perché Serve](#2-cosè-numpy-e-perché-serve)
|
||||
3. [Concetti Base di NumPy](#3-concetti-base-di-numpy)
|
||||
4. [Dal Tuo Codice a NumPy: Caso Pratico](#4-dal-tuo-codice-a-numpy-caso-pratico)
|
||||
5. [Spatial Hashing: L'Algoritmo Intelligente](#5-spatial-hashing-lalgoritmo-intelligente)
|
||||
6. [Operazioni Vettoriali in NumPy](#6-operazioni-vettoriali-in-numpy)
|
||||
7. [Best Practices e Pitfalls](#7-best-practices-e-pitfalls)
|
||||
|
||||
---
|
||||
|
||||
## 1. Introduzione: Il Problema delle Performance
|
||||
|
||||
### Il Tuo Sistema Originale (Funzionava Bene!)
|
||||
|
||||
```python
|
||||
# rats.py - Il tuo approccio originale
|
||||
def update_maze(self):
|
||||
# Popolava dizionari con le posizioni delle unità
|
||||
self.unit_positions = {}
|
||||
self.unit_positions_before = {}
|
||||
|
||||
for unit in self.units.values():
|
||||
unit.move()
|
||||
# Raggruppa unità per posizione
|
||||
self.unit_positions.setdefault(unit.position, []).append(unit)
|
||||
self.unit_positions_before.setdefault(unit.position_before, []).append(unit)
|
||||
|
||||
for unit in self.units.values():
|
||||
unit.collisions() # Ogni unità controlla le proprie collisioni
|
||||
```
|
||||
|
||||
### Il Problema con 200+ Unità
|
||||
|
||||
Con 5-10 topi: **funziona perfetto** ✅
|
||||
Con 200+ topi: **FPS crollano** ❌
|
||||
|
||||
**Perché?**
|
||||
- Ogni topo controlla collisioni con TUTTI gli altri topi
|
||||
- 200 topi = 200 × 200 = **40,000 controlli per frame!**
|
||||
- Complessità: **O(n²)** - cresce in modo quadratico
|
||||
|
||||
---
|
||||
|
||||
## 2. Cos'è NumPy e Perché Serve
|
||||
|
||||
### NumPy in 3 Parole
|
||||
**Array multidimensionali ottimizzati**
|
||||
|
||||
### Perché è Veloce?
|
||||
|
||||
```python
|
||||
# Python puro (lento ❌)
|
||||
distances = []
|
||||
for i in range(1000):
|
||||
for j in range(1000):
|
||||
dx = x[i] - y[j]
|
||||
dy = x[i] - y[j]
|
||||
distances.append((dx**2 + dy**2)**0.5)
|
||||
# Tempo: ~500ms con 1 milione di operazioni
|
||||
|
||||
# NumPy (veloce ✅)
|
||||
import numpy as np
|
||||
distances = np.sqrt((x[:, None] - y[None, :])**2 + (x[:, None] - y[None, :])**2)
|
||||
# Tempo: ~5ms - 100 volte più veloce!
|
||||
```
|
||||
|
||||
### Perché la Differenza?
|
||||
|
||||
1. **Codice C Compilato**: NumPy è scritto in C/C++, non Python interpretato
|
||||
2. **Operazioni Vettoriali**: Calcola migliaia di valori in parallelo
|
||||
3. **Memoria Contigua**: Dati organizzati efficientemente in RAM
|
||||
4. **CPU SIMD**: Usa istruzioni speciali della CPU per parallelismo hardware
|
||||
|
||||
---
|
||||
|
||||
## 3. Concetti Base di NumPy
|
||||
|
||||
### Array vs Liste Python
|
||||
|
||||
```python
|
||||
# Lista Python (flessibile ma lenta)
|
||||
lista = [1, 2, 3, 4, 5]
|
||||
lista.append("sei") # OK - tipi misti
|
||||
lista[0] = "uno" # OK - cambio tipo
|
||||
|
||||
# Array NumPy (veloce ma rigido)
|
||||
import numpy as np
|
||||
array = np.array([1, 2, 3, 4, 5])
|
||||
# array[0] = "uno" # ERRORE! Tipo fisso: int64
|
||||
```
|
||||
|
||||
**Regola**: NumPy sacrifica flessibilità per velocità
|
||||
|
||||
### Operazioni Elemento per Elemento
|
||||
|
||||
```python
|
||||
# Python puro
|
||||
lista_a = [1, 2, 3]
|
||||
lista_b = [4, 5, 6]
|
||||
risultato = []
|
||||
for a, b in zip(lista_a, lista_b):
|
||||
risultato.append(a + b)
|
||||
# risultato = [5, 7, 9]
|
||||
|
||||
# NumPy (broadcasting)
|
||||
array_a = np.array([1, 2, 3])
|
||||
array_b = np.array([4, 5, 6])
|
||||
risultato = array_a + array_b # [5, 7, 9] - automatico!
|
||||
```
|
||||
|
||||
### Broadcasting: Operazioni su Array di Dimensioni Diverse
|
||||
|
||||
```python
|
||||
# Esempio reale dal tuo gioco: calcolare distanze
|
||||
unit_positions = np.array([[10, 20], [30, 40], [50, 60]]) # 3 unità
|
||||
target = np.array([25, 35]) # 1 bersaglio
|
||||
|
||||
# Vogliamo: distanza di ogni unità dal bersaglio
|
||||
# Senza broadcasting (noioso):
|
||||
distances = []
|
||||
for pos in unit_positions:
|
||||
dx = pos[0] - target[0]
|
||||
dy = pos[1] - target[1]
|
||||
distances.append(np.sqrt(dx**2 + dy**2))
|
||||
|
||||
# Con broadcasting (elegante):
|
||||
diff = unit_positions - target # NumPy espande target automaticamente
|
||||
distances = np.sqrt((diff**2).sum(axis=1))
|
||||
# Output: [18.03, 7.07, 28.28]
|
||||
```
|
||||
|
||||
**Come funziona?**
|
||||
```
|
||||
unit_positions: [[10, 20], target: [25, 35]
|
||||
[30, 40],
|
||||
[50, 60]] Broadcasting lo espande a:
|
||||
[[25, 35],
|
||||
[25, 35],
|
||||
[25, 35]]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Dal Tuo Codice a NumPy: Caso Pratico
|
||||
|
||||
### Fase 1: Il Tuo Approccio con i Dizionari
|
||||
|
||||
```python
|
||||
# units/rat.py - Il tuo codice originale
|
||||
def collisions(self):
|
||||
# Prende unità nella stessa cella
|
||||
units_here = self.game.unit_positions.get(self.position, [])
|
||||
units_before = self.game.unit_positions_before.get(self.position_before, [])
|
||||
|
||||
# Controlla ogni unità
|
||||
for other_unit in units_here + units_before:
|
||||
if other_unit.id == self.id:
|
||||
continue
|
||||
|
||||
# Logica di collisione...
|
||||
if self.sex == other_unit.sex and self.fight:
|
||||
self.die(other_unit)
|
||||
elif self.sex != other_unit.sex:
|
||||
self.fuck(other_unit)
|
||||
```
|
||||
|
||||
**Pro del Tuo Approccio:**
|
||||
- ✅ Semplice e leggibile
|
||||
- ✅ Usa dizionari Python nativi
|
||||
- ✅ Funziona perfettamente con poche unità
|
||||
|
||||
**Problema con 200+ Unità:**
|
||||
- ❌ Ogni topo itera su liste di Python
|
||||
- ❌ Controlli ripetuti (topo A controlla B, poi B controlla A)
|
||||
- ❌ Nessuna ottimizzazione per distanze
|
||||
|
||||
### Fase 2: Spatial Hashing (L'Idea Geniale)
|
||||
|
||||
Prima di NumPy, serve un algoritmo migliore: **Spatial Hashing**
|
||||
|
||||
```python
|
||||
# Concetto: dividi il mondo in "celle" (griglia)
|
||||
# Ogni cella contiene solo le unità al suo interno
|
||||
|
||||
# Mondo di gioco:
|
||||
# 0 1 2 3
|
||||
# 0 [ ] [ ] [ ] [ ]
|
||||
# 1 [ ] [A] [B] [ ]
|
||||
# 2 [ ] [ ] [C] [ ]
|
||||
# 3 [ ] [ ] [ ] [ ]
|
||||
|
||||
# Dizionario spatial hash:
|
||||
spatial_grid = {
|
||||
(1, 1): [unit_A],
|
||||
(2, 1): [unit_B],
|
||||
(2, 2): [unit_C]
|
||||
}
|
||||
|
||||
# Quando unit_A cerca collisioni:
|
||||
# Controlla SOLO celle (1,1) e adiacenti (0,0), (0,1), (0,2), (1,0), (1,2), (2,0), (2,1), (2,2)
|
||||
# Non controlla unit_C a (2,2) - troppo lontano!
|
||||
```
|
||||
|
||||
**Vantaggio**: Da O(n²) a O(n)!
|
||||
- 200 unità: da 40,000 controlli a ~1,800 controlli (celle adiacenti)
|
||||
|
||||
### Fase 3: NumPy per Calcoli Massivi
|
||||
|
||||
```python
|
||||
# engine/collision_system.py - Il nuovo approccio
|
||||
|
||||
class CollisionSystem:
|
||||
def __init__(self, cell_size=32):
|
||||
# Pre-allocazione: prepara spazio per array NumPy
|
||||
self.unit_ids = np.zeros(100, dtype=np.int64) # Array di ID
|
||||
self.bboxes = np.zeros((100, 4), dtype=np.float32) # Array di bounding box
|
||||
self.positions = np.zeros((100, 2), dtype=np.int32) # Array di posizioni
|
||||
self.current_size = 0 # Quante unità registrate
|
||||
self.capacity = 100 # Capacità massima prima di resize
|
||||
```
|
||||
|
||||
**Perché Pre-allocazione?**
|
||||
```python
|
||||
# Cattivo: crescita lenta ❌
|
||||
array = np.array([])
|
||||
for i in range(1000):
|
||||
array = np.append(array, i) # Crea NUOVO array ogni volta!
|
||||
# Tempo: ~200ms
|
||||
|
||||
# Buono: pre-allocazione ✅
|
||||
array = np.zeros(1000)
|
||||
for i in range(1000):
|
||||
array[i] = i # Modifica array esistente
|
||||
# Tempo: ~2ms
|
||||
```
|
||||
|
||||
### Fase 4: Registrazione Unità
|
||||
|
||||
```python
|
||||
def register_unit(self, unit_id, bbox, position, position_before, collision_layer):
|
||||
"""Registra un'unità nel sistema di collisione"""
|
||||
|
||||
# Se array pieno, raddoppia capacità
|
||||
if self.current_size >= self.capacity:
|
||||
self._resize_arrays(self.capacity * 2)
|
||||
|
||||
idx = self.current_size
|
||||
|
||||
# Inserisci dati negli array NumPy
|
||||
self.unit_ids[idx] = unit_id
|
||||
self.bboxes[idx] = bbox # [x1, y1, x2, y2]
|
||||
self.positions[idx] = position
|
||||
self.position_before[idx] = position_before
|
||||
self.layers[idx] = collision_layer.value
|
||||
|
||||
# Spatial hashing: aggiungi a griglia
|
||||
cell = (position[0], position[1])
|
||||
self.spatial_grid[cell].append(idx) # Salva INDICE, non unità
|
||||
|
||||
self.current_size += 1
|
||||
```
|
||||
|
||||
**Nota Importante**: Salviamo **indici** negli array, non oggetti Python!
|
||||
- `spatial_grid[(5, 10)] = [0, 3, 7]` → Unità agli indici 0, 3, 7 degli array NumPy
|
||||
- Accesso veloce: `self.bboxes[0]`, `self.bboxes[3]`, `self.bboxes[7]`
|
||||
|
||||
### Fase 5: Collisioni Vettoriali con NumPy
|
||||
|
||||
```python
|
||||
def get_collisions_for_unit(self, unit_id, bbox, collision_layer):
|
||||
"""Trova tutte le collisioni per un'unità"""
|
||||
|
||||
# 1. Trova celle da controllare (spatial hashing)
|
||||
x, y = bbox[0] // self.cell_size, bbox[1] // self.cell_size
|
||||
cells_to_check = [
|
||||
(x-1, y-1), (x, y-1), (x+1, y-1),
|
||||
(x-1, y), (x, y), (x+1, y),
|
||||
(x-1, y+1), (x, y+1), (x+1, y+1)
|
||||
]
|
||||
|
||||
# 2. Raccogli candidati da celle adiacenti
|
||||
candidates = []
|
||||
for cell in cells_to_check:
|
||||
candidates.extend(self.spatial_grid.get(cell, []))
|
||||
|
||||
if len(candidates) < 10:
|
||||
# POCHI candidati: usa Python normale
|
||||
collisions = []
|
||||
for idx in candidates:
|
||||
if self.unit_ids[idx] == unit_id:
|
||||
continue
|
||||
if self._check_bbox_collision(bbox, self.bboxes[idx]):
|
||||
collisions.append((self.layers[idx], self.unit_ids[idx]))
|
||||
return collisions
|
||||
|
||||
else:
|
||||
# MOLTI candidati: USA NUMPY! ✨
|
||||
return self._vectorized_collision_check(unit_id, bbox, candidates)
|
||||
```
|
||||
|
||||
### Fase 6: La Magia di NumPy - Vectorized Collision Check
|
||||
|
||||
```python
|
||||
def _vectorized_collision_check(self, unit_id, bbox, candidate_indices):
|
||||
"""Controlla collisioni usando NumPy per massima velocità"""
|
||||
|
||||
# Converti candidati in array NumPy
|
||||
candidate_indices = np.array(candidate_indices, dtype=np.int32)
|
||||
|
||||
# Filtra l'unità stessa (non collidere con se stessi)
|
||||
mask = self.unit_ids[candidate_indices] != unit_id
|
||||
candidate_indices = candidate_indices[mask]
|
||||
|
||||
if len(candidate_indices) == 0:
|
||||
return []
|
||||
|
||||
# Estrai bounding box di TUTTI i candidati in un colpo solo
|
||||
candidate_bboxes = self.bboxes[candidate_indices] # Shape: (N, 4)
|
||||
# candidate_bboxes = [[x1, y1, x2, y2], # candidato 0
|
||||
# [x1, y1, x2, y2], # candidato 1
|
||||
# ...]
|
||||
|
||||
# Controllo collisione AABB (Axis-Aligned Bounding Box)
|
||||
# Due rettangoli collidono se:
|
||||
# - bbox.x1 < other.x2 AND
|
||||
# - bbox.x2 > other.x1 AND
|
||||
# - bbox.y1 < other.y2 AND
|
||||
# - bbox.y2 > other.y1
|
||||
|
||||
# NumPy calcola TUTTE le collisioni contemporaneamente! 🚀
|
||||
colliding_mask = (
|
||||
(bbox[0] < candidate_bboxes[:, 2]) & # bbox.x1 < others.x2
|
||||
(bbox[2] > candidate_bboxes[:, 0]) & # bbox.x2 > others.x1
|
||||
(bbox[1] < candidate_bboxes[:, 3]) & # bbox.y1 < others.y2
|
||||
(bbox[3] > candidate_bboxes[:, 1]) # bbox.y2 > others.y1
|
||||
)
|
||||
# colliding_mask = [True, False, True, False, True, ...]
|
||||
|
||||
# Filtra solo unità che collidono
|
||||
colliding_indices = candidate_indices[colliding_mask]
|
||||
|
||||
# Restituisci coppie (layer, unit_id)
|
||||
return list(zip(
|
||||
self.layers[colliding_indices],
|
||||
self.unit_ids[colliding_indices]
|
||||
))
|
||||
```
|
||||
|
||||
**Spiegazione Dettagliata del Codice NumPy:**
|
||||
|
||||
```python
|
||||
# Esempio concreto con 3 candidati
|
||||
bbox = [10, 20, 30, 40] # Nostro topo: x1=10, y1=20, x2=30, y2=40
|
||||
|
||||
candidate_bboxes = np.array([
|
||||
[5, 15, 25, 35], # Candidato 0
|
||||
[50, 60, 70, 80], # Candidato 1 (lontano)
|
||||
[15, 25, 35, 45] # Candidato 2
|
||||
])
|
||||
|
||||
# Controllo bbox[0] < candidate_bboxes[:, 2]
|
||||
# bbox[0] = 10
|
||||
# candidate_bboxes[:, 2] = [25, 70, 35] # Colonna x2 di tutti i candidati
|
||||
# 10 < [25, 70, 35] = [True, True, True]
|
||||
|
||||
# Controllo bbox[2] > candidate_bboxes[:, 0]
|
||||
# bbox[2] = 30
|
||||
# candidate_bboxes[:, 0] = [5, 50, 15] # Colonna x1
|
||||
# 30 > [5, 50, 15] = [True, False, True]
|
||||
|
||||
# ... altri controlli ...
|
||||
|
||||
# Combinazione finale (AND logico):
|
||||
colliding_mask = [True, False, True] # Solo 0 e 2 collidono!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Spatial Hashing: L'Algoritmo Intelligente
|
||||
|
||||
### Visualizzazione Pratica
|
||||
|
||||
```
|
||||
Mondo di gioco 640x480, cell_size=32
|
||||
|
||||
Griglia spaziale:
|
||||
0 1 2 3 4 5 ... 19
|
||||
┌────┬────┬────┬────┬────┬────┬────┬────┐
|
||||
0 │ │ │ │ │ │ │ │ │
|
||||
├────┼────┼────┼────┼────┼────┼────┼────┤
|
||||
1 │ │ R1 │ R2 │ │ │ │ │ │ R = Rat
|
||||
├────┼────┼────┼────┼────┼────┼────┼────┤ B = Bomb
|
||||
2 │ │ R3 │ B1 │ R4 │ │ │ │ │ M = Mine
|
||||
├────┼────┼────┼────┼────┼────┼────┼────┤
|
||||
3 │ │ │ M1 │ │ │ │ │ │
|
||||
└────┴────┴────┴────┴────┴────┴────┴────┘
|
||||
```
|
||||
|
||||
### Come Funziona il Lookup
|
||||
|
||||
```python
|
||||
# R1 cerca collisioni da cella (1, 1)
|
||||
def get_collisions_for_unit(self, unit_id, bbox):
|
||||
x, y = 1, 1 # Posizione R1
|
||||
|
||||
# Controlla 9 celle (3x3 centrato su R1):
|
||||
cells = [
|
||||
(0,0), (1,0), (2,0), # Riga sopra
|
||||
(0,1), (1,1), (2,1), # Riga centrale (include R1)
|
||||
(0,2), (1,2), (2,2) # Riga sotto
|
||||
]
|
||||
|
||||
# spatial_grid è un dizionario:
|
||||
# {
|
||||
# (1, 1): [idx_R1],
|
||||
# (2, 1): [idx_R2],
|
||||
# (1, 2): [idx_R3],
|
||||
# (2, 2): [idx_B1, idx_R4],
|
||||
# (2, 3): [idx_M1]
|
||||
# }
|
||||
|
||||
candidates = []
|
||||
for cell in cells:
|
||||
candidates.extend(self.spatial_grid.get(cell, []))
|
||||
|
||||
# candidates = [idx_R1, idx_R2, idx_R3, idx_B1, idx_R4]
|
||||
# NON include idx_M1 perché (2,3) è fuori dal range 3x3!
|
||||
```
|
||||
|
||||
### Benefici Misurabili
|
||||
|
||||
```python
|
||||
# SENZA spatial hashing (O(n²)):
|
||||
# 200 unità → 200 × 200 = 40,000 controlli
|
||||
|
||||
# CON spatial hashing (O(n)):
|
||||
# 200 unità, distribuite su 20×15=300 celle
|
||||
# Media 0.67 unità per cella
|
||||
# Ogni unità controlla 9 celle × 0.67 = ~6 candidati
|
||||
# 200 unità × 6 candidati = 1,200 controlli
|
||||
#
|
||||
# Miglioramento: 40,000 → 1,200 = 33x più veloce! 🚀
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Operazioni Vettoriali in NumPy
|
||||
|
||||
### Broadcasting Avanzato: Esplosioni
|
||||
|
||||
```python
|
||||
def get_units_in_area(self, positions, layer_filter=None):
|
||||
"""Trova unità in un'area (es. esplosione bomba)"""
|
||||
|
||||
# positions: lista di posizioni esplose
|
||||
# es. [(10, 10), (10, 11), (11, 10), (11, 11)] # Esplosione 2x2
|
||||
|
||||
if self.current_size == 0:
|
||||
return []
|
||||
|
||||
# Converti in array NumPy
|
||||
area_positions = np.array(positions, dtype=np.int32) # Shape: (4, 2)
|
||||
|
||||
# Prendi posizioni di TUTTE le unità
|
||||
all_positions = self.positions[:self.current_size] # Shape: (N, 2)
|
||||
# es. all_positions = [[5, 5], [10, 10], [15, 15], [10, 11], ...]
|
||||
|
||||
# Broadcasting trick per confrontare OGNI posizione esplosione con OGNI unità
|
||||
# area_positions[:, None, :] → Shape: (4, 1, 2)
|
||||
# all_positions[None, :, :] → Shape: (1, N, 2)
|
||||
# Risultato → Shape: (4, N, 2) - tutte le combinazioni!
|
||||
|
||||
matches = (area_positions[:, None, :] == all_positions[None, :, :]).all(axis=2)
|
||||
# matches[i, j] = True se esplosione i colpisce unità j
|
||||
|
||||
# any(axis=0): almeno una posizione esplosione colpisce quella unità?
|
||||
unit_hit_mask = matches.any(axis=0) # Shape: (N,)
|
||||
|
||||
# Filtra per layer se richiesto
|
||||
if layer_filter:
|
||||
valid_layers = self.layers[:self.current_size] == layer_filter.value
|
||||
unit_hit_mask = unit_hit_mask & valid_layers
|
||||
|
||||
# Restituisci ID delle unità colpite
|
||||
hit_indices = np.where(unit_hit_mask)[0]
|
||||
return self.unit_ids[hit_indices].tolist()
|
||||
```
|
||||
|
||||
**Spiegazione con Esempio Concreto:**
|
||||
|
||||
```python
|
||||
# Bomba esplode creando 4 celle di fuoco
|
||||
area_positions = np.array([[10, 10], [10, 11], [11, 10], [11, 11]])
|
||||
|
||||
# Ci sono 3 topi nel gioco
|
||||
all_positions = np.array([[5, 5], [10, 10], [10, 11]])
|
||||
|
||||
# Broadcasting:
|
||||
area_positions[:, None, :].shape # (4, 1, 2)
|
||||
all_positions[None, :, :].shape # (1, 3, 2)
|
||||
|
||||
# Confronto elemento per elemento:
|
||||
matches = (area_positions[:, None, :] == all_positions[None, :, :]).all(axis=2)
|
||||
# matches = [
|
||||
# [False, False, False], # Esplosione (10,10) vs [(5,5), (10,10), (10,11)]
|
||||
# [False, True, False], # Esplosione (10,11) vs ...
|
||||
# [False, False, True ], # Esplosione (11,10) vs ...
|
||||
# [False, False, False] # Esplosione (11,11) vs ...
|
||||
# ]
|
||||
|
||||
# Collassa su asse 0 (almeno UNA esplosione colpisce?)
|
||||
unit_hit_mask = matches.any(axis=0) # [False, True, True]
|
||||
# Topo 0 (5,5): NON colpito
|
||||
# Topo 1 (10,10): COLPITO (da esplosione 0)
|
||||
# Topo 2 (10,11): COLPITO (da esplosione 1)
|
||||
```
|
||||
|
||||
### Calcolo Distanze Vettoriale
|
||||
|
||||
```python
|
||||
# Esempio: trovare tutti i topi entro raggio 50 pixel da una bomba
|
||||
|
||||
bomb_position = np.array([100, 100]) # Posizione bomba
|
||||
|
||||
# Posizioni di tutti i topi (array NumPy)
|
||||
rat_positions = self.positions[:self.current_size] # Shape: (N, 2)
|
||||
|
||||
# Calcolo distanze usando broadcasting
|
||||
diff = rat_positions - bomb_position # Shape: (N, 2)
|
||||
# diff[i] = [rat_x - bomb_x, rat_y - bomb_y]
|
||||
|
||||
distances = np.sqrt((diff ** 2).sum(axis=1)) # Shape: (N,)
|
||||
# distances[i] = sqrt((dx)^2 + (dy)^2)
|
||||
|
||||
# Trova topi entro raggio
|
||||
within_radius = distances < 50 # Boolean mask
|
||||
hit_rat_indices = np.where(within_radius)[0]
|
||||
|
||||
# Esempio output:
|
||||
# rat_positions = [[90, 90], [110, 110], [200, 200]]
|
||||
# diff = [[-10, -10], [10, 10], [100, 100]]
|
||||
# distances = [14.14, 14.14, 141.42]
|
||||
# within_radius = [True, True, False]
|
||||
# hit_rat_indices = [0, 1] # Primi due topi colpiti!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Best Practices e Pitfalls
|
||||
|
||||
### ✅ Quando Usare NumPy
|
||||
|
||||
```python
|
||||
# BUONO: Operazioni su molti dati
|
||||
positions = np.array([[...] for _ in range(1000)])
|
||||
distances = np.sqrt(((positions - target)**2).sum(axis=1))
|
||||
|
||||
# CATTIVO: Operazioni su pochi dati (overhead NumPy!)
|
||||
positions = np.array([[10, 20], [30, 40]]) # Solo 2 elementi
|
||||
distances = np.sqrt(((positions - target)**2).sum(axis=1))
|
||||
# Più lento di un semplice loop Python!
|
||||
```
|
||||
|
||||
**Regola nel tuo codice:**
|
||||
```python
|
||||
if len(candidates) < 10:
|
||||
# Usa Python normale
|
||||
for idx in candidates:
|
||||
...
|
||||
else:
|
||||
# Usa NumPy
|
||||
self._vectorized_collision_check(...)
|
||||
```
|
||||
|
||||
### ✅ Pre-allocazione vs Append
|
||||
|
||||
```python
|
||||
# CATTIVO ❌ (lento con array grandi)
|
||||
array = np.array([])
|
||||
for i in range(10000):
|
||||
array = np.append(array, i) # O(n) ad ogni append!
|
||||
|
||||
# BUONO ✅ (veloce)
|
||||
array = np.zeros(10000)
|
||||
for i in range(10000):
|
||||
array[i] = i # O(1) ad ogni assegnazione
|
||||
|
||||
# MIGLIORE ✅ (senza loop!)
|
||||
array = np.arange(10000) # Operazione vettoriale nativa
|
||||
```
|
||||
|
||||
### ✅ Memory Layout e Performance
|
||||
|
||||
```python
|
||||
# Row-major (C order) - default NumPy
|
||||
array = np.zeros((1000, 3), order='C')
|
||||
# Memoria: [x0, y0, z0, x1, y1, z1, ...]
|
||||
# Veloce per accesso righe: array[i, :]
|
||||
|
||||
# Column-major (Fortran order)
|
||||
array = np.zeros((1000, 3), order='F')
|
||||
# Memoria: [x0, x1, ..., y0, y1, ..., z0, z1, ...]
|
||||
# Veloce per accesso colonne: array[:, j]
|
||||
|
||||
# Nel tuo caso (posizioni):
|
||||
self.positions = np.zeros((100, 2)) # Row-major è perfetto
|
||||
# Accesso frequente: self.positions[idx] → [x, y] di un'unità
|
||||
```
|
||||
|
||||
### ⚠️ Pitfall Comuni
|
||||
|
||||
#### 1. Copy vs View
|
||||
|
||||
```python
|
||||
# View (condivide memoria)
|
||||
a = np.array([1, 2, 3])
|
||||
b = a[:] # b è una VIEW di a
|
||||
b[0] = 999
|
||||
print(a) # [999, 2, 3] - modificato anche a!
|
||||
|
||||
# Copy (memoria separata)
|
||||
a = np.array([1, 2, 3])
|
||||
b = a.copy()
|
||||
b[0] = 999
|
||||
print(a) # [1, 2, 3] - a è immutato
|
||||
```
|
||||
|
||||
**Nel tuo codice:**
|
||||
```python
|
||||
def get_collisions_for_unit(self, ...):
|
||||
# Filtriamo candidati
|
||||
candidate_indices = candidate_indices[mask] # Crea VIEW
|
||||
|
||||
# Se modifichi candidate_indices dopo, potresti modificare l'originale!
|
||||
# Soluzione: .copy() se necessario
|
||||
```
|
||||
|
||||
#### 2. Broadcasting Inatteso
|
||||
|
||||
```python
|
||||
a = np.array([1, 2, 3])
|
||||
b = np.array([[1], [2], [3]])
|
||||
|
||||
# Cosa succede?
|
||||
result = a + b
|
||||
# Broadcasting espande a:
|
||||
# [[1, 2, 3], [[1], [1], [1]] [[2, 3, 4],
|
||||
# [1, 2, 3], + [2], [2], [2]] = [3, 4, 5],
|
||||
# [1, 2, 3]] [3], [3], [3]] [4, 5, 6]]
|
||||
```
|
||||
|
||||
**Verifica sempre le shape:**
|
||||
```python
|
||||
print(f"Shape: {array.shape}") # Sempre prima di operazioni complesse!
|
||||
```
|
||||
|
||||
#### 3. Integer Overflow
|
||||
|
||||
```python
|
||||
# ATTENZIONE con dtype piccoli!
|
||||
a = np.array([250], dtype=np.uint8) # Max 255
|
||||
b = a + 10 # 260, ma uint8 wrap: diventa 4!
|
||||
|
||||
# Soluzione: usa dtype appropriati
|
||||
a = np.array([250], dtype=np.int32) # Max ~2 miliardi
|
||||
b = a + 10 # 260 ✅
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Confronto Finale: Prima vs Dopo
|
||||
|
||||
### Codice Originale (Il Tuo)
|
||||
|
||||
```python
|
||||
# rats.py
|
||||
def update_maze(self):
|
||||
self.unit_positions = {}
|
||||
self.unit_positions_before = {}
|
||||
|
||||
for unit in self.units.values():
|
||||
unit.move()
|
||||
self.unit_positions.setdefault(unit.position, []).append(unit)
|
||||
self.unit_positions_before.setdefault(unit.position_before, []).append(unit)
|
||||
|
||||
for unit in self.units.values():
|
||||
unit.collisions()
|
||||
|
||||
# units/rat.py
|
||||
def collisions(self):
|
||||
for other_unit in self.game.unit_positions.get(self.position, []):
|
||||
if other_unit.id == self.id:
|
||||
continue
|
||||
# Controlla collisione...
|
||||
```
|
||||
|
||||
**Complessità**: O(n²) nel caso peggiore
|
||||
**Performance**: ~50ms con 200 unità
|
||||
**Memoria**: Dizionari Python + liste di oggetti
|
||||
|
||||
### Codice Ottimizzato (NumPy)
|
||||
|
||||
```python
|
||||
# rats.py - 4-pass loop
|
||||
def update_maze(self):
|
||||
self.collision_system.clear()
|
||||
|
||||
# Pass 1: Pre-registra posizioni
|
||||
for unit in self.units.values():
|
||||
self.collision_system.register_unit(unit.id, unit.bbox, ...)
|
||||
|
||||
# Pass 2: Movimento
|
||||
for unit in self.units.values():
|
||||
unit.move()
|
||||
|
||||
# Pass 3: Ri-registra dopo movimento
|
||||
self.collision_system.clear()
|
||||
for unit in self.units.values():
|
||||
self.collision_system.register_unit(unit.id, unit.bbox, ...)
|
||||
|
||||
# Pass 4: Collisioni
|
||||
for unit in self.units.values():
|
||||
unit.collisions()
|
||||
|
||||
# units/rat.py
|
||||
def collisions(self):
|
||||
collisions = self.game.collision_system.get_collisions_for_unit(
|
||||
self.id, self.bbox, self.collision_layer
|
||||
)
|
||||
for _, other_id in collisions:
|
||||
other_unit = self.game.get_unit_by_id(other_id)
|
||||
if isinstance(other_unit, Rat):
|
||||
# Controlla collisione...
|
||||
```
|
||||
|
||||
**Complessità**: O(n) con spatial hashing + NumPy
|
||||
**Performance**: ~3ms con 250 unità (16x più veloce!)
|
||||
**Memoria**: Array NumPy pre-allocati (più efficiente)
|
||||
|
||||
---
|
||||
|
||||
## Conclusione
|
||||
|
||||
### Cosa Hai Imparato
|
||||
|
||||
1. **NumPy**: Array veloci per operazioni matematiche massive
|
||||
2. **Broadcasting**: Operazioni automatiche su array di dimensioni diverse
|
||||
3. **Spatial Hashing**: Ridurre O(n²) a O(n) con partizionamento spaziale
|
||||
4. **Vectorization**: Sostituire loop Python con operazioni NumPy parallele
|
||||
5. **Pre-allocazione**: Evitare allocazioni ripetute per velocità
|
||||
|
||||
### Quando Applicare Queste Tecniche
|
||||
|
||||
- ✅ **Usa NumPy quando**: Hai 50+ elementi da processare con operazioni matematiche
|
||||
- ✅ **Usa Spatial Hashing quando**: Controlli collisioni/prossimità in spazio 2D/3D
|
||||
- ✅ **Usa Vectorization quando**: Stesso calcolo ripetuto su molti dati
|
||||
- ❌ **Non usare quando**: Pochi elementi (< 10) o logica complessa non matematica
|
||||
|
||||
### Risorse per Approfondire
|
||||
|
||||
- **NumPy Documentation**: https://numpy.org/doc/stable/
|
||||
- **NumPy Quickstart**: https://numpy.org/doc/stable/user/quickstart.html
|
||||
- **Broadcasting**: https://numpy.org/doc/stable/user/basics.broadcasting.html
|
||||
- **Performance Tips**: https://numpy.org/doc/stable/user/c-info.performance.html
|
||||
|
||||
---
|
||||
|
||||
**Il tuo codice originale era ottimo per il caso d'uso iniziale.** L'ottimizzazione con NumPy è stata necessaria solo quando hai scalato a 200+ unità. Questo è un esempio perfetto di "ottimizza quando serve", non prematuramente! 🎯
|
||||
@@ -8,6 +8,7 @@ Mice! is a strategic game where players must kill rats with bombs before they re
|
||||
## Features
|
||||
|
||||
- **Maze Generation**: Randomly generated mazes using Depth First Search (DFS) algorithm.
|
||||
- **Original Level Support**: Loads the original `level.dat` from `assets/Rat/level.dat` when present and falls back to `maze.json` otherwise.
|
||||
- **Units**: Different types of units such as rats, bombs, and points with specific behaviors.
|
||||
- **Graphics**: Custom graphics for maze tiles, units, and effects.
|
||||
- **Sound Effects**: Audio feedback for various game events.
|
||||
@@ -72,6 +73,7 @@ The Mice! game engine is built on a modular architecture designed for flexibilit
|
||||
- **Map Class**: Manages the game world structure
|
||||
- **Features**:
|
||||
- Maze data loading and parsing
|
||||
- DAT archive parsing for the original 32 built-in RATS levels
|
||||
- Collision detection system
|
||||
- Tile-based world representation
|
||||
- Pathfinding support for AI units
|
||||
@@ -238,6 +240,20 @@ Units interact through a centralized collision and event system:
|
||||
- **Libraries**:
|
||||
- `numpy` 2.3.4 for vectorized collision detection
|
||||
- `sdl2` for graphics and window management
|
||||
|
||||
## Level Sources
|
||||
|
||||
- Preferred source: `assets/Rat/level.dat`
|
||||
- Fallback source: `maze.json`
|
||||
- Current behavior: the loader can read any level from the DAT archive via `--level N`, while still falling back to `maze.json` when the DAT is unavailable.
|
||||
- Tile semantics are now preserved internally from the original format: `0=EMPTY`, `1=WALL`, `2=TUNNEL`.
|
||||
- Rendering uses those semantics directly: walls use themed grass/flower tiles, tunnel cells use themed cave tiles, and empty cells remain the generic walkable tunnel floor used by the Python version.
|
||||
|
||||
### Run examples
|
||||
|
||||
- `python rats.py`
|
||||
- `python rats.py --level 7`
|
||||
- `python rats.py --map maze.json`
|
||||
- `Pillow` for image processing
|
||||
- `uuid` for unique unit identification
|
||||
- `subprocess` for playing sound effects
|
||||
|
||||
@@ -0,0 +1,419 @@
|
||||
# regenerate_background() Analysis And Refactor Plan
|
||||
|
||||
## Scope
|
||||
|
||||
This document analyzes `Graphics.regenerate_background()` and proposes a staged refactor plan.
|
||||
|
||||
Relevant code paths:
|
||||
|
||||
- `engine/graphics.py#L146` `draw_maze()` lazily triggers background generation.
|
||||
- `engine/graphics.py#L155` `draw_cave_foreground()` consumes cave overlay metadata produced during regeneration.
|
||||
- `engine/graphics.py#L175` `regenerate_background()` builds the static background texture and cave overlay placements.
|
||||
- `engine/graphics.py#L311` `add_blood_stain()` confirms that blood is intentionally excluded from the background texture and rendered as a separate overlay.
|
||||
- `engine/sdl2.py#L98` `create_texture()` composites surface tiles into one SDL texture.
|
||||
- `engine/sdl2.py#L118` `load_image()` explains why the code keeps both surfaces and textures for the same themed assets.
|
||||
- `engine/maze.py#L13-L15` define `MAP_EMPTY`, `MAP_WALL`, and `MAP_TUNNEL`.
|
||||
- `rats.py#L79`, `rats.py#L117-L121`, and `rats.py#L163-L165` show where background state is invalidated.
|
||||
- `rats.py#L335` shows cave foreground rendering happens after the background draw and before units are drawn.
|
||||
|
||||
## What The Method Actually Does
|
||||
|
||||
`regenerate_background()` is doing more than the name suggests. It is not only “regenerating a background”; it is handling five separate concerns in one place:
|
||||
|
||||
1. It walks the logical map cell by cell.
|
||||
2. It analyzes neighborhood topology around each wall or tunnel cell.
|
||||
3. It chooses visual variants, including random grass and flower decoration.
|
||||
4. It builds cave foreground overlay metadata for later explosion-aware rendering.
|
||||
5. It commits the accumulated surfaces into a single SDL background texture.
|
||||
|
||||
That makes it both a planner and a renderer.
|
||||
|
||||
## Current Inputs, Outputs, And Side Effects
|
||||
|
||||
### Inputs read from `self`
|
||||
|
||||
- `self.map.tiles`, `self.map.width`, `self.map.height`
|
||||
- `self.cell_size`
|
||||
- `self.grasses`, `self.grass_textures`
|
||||
- `self.flowers`, `self.flower_textures`
|
||||
- `self.edges`, `self.corners`, `self.inner_corners`
|
||||
- `self.caves`
|
||||
- `self.render_engine`
|
||||
|
||||
### Derived helpers inside the method
|
||||
|
||||
- `occupied(x, y)` treats every non-empty cell as occupied, so both walls and tunnels count as solid neighbors for topology decisions.
|
||||
- `is_tunnel(x, y)` is used only for the flower suppression logic in the bottom-right quadrant.
|
||||
- `draw(...)` appends background surface tiles.
|
||||
- `draw_cave(...)` appends cave overlay tuples in the format consumed later by `draw_cave_foreground()`.
|
||||
- `random_wall()`, `random_wall_texture()`, `random_flower()`, `random_flower_texture()` embed random selection directly in the traversal logic.
|
||||
|
||||
### Outputs and side effects
|
||||
|
||||
- Resets `self.cave_foreground_tiles`
|
||||
- Builds a local `texture_tiles` list
|
||||
- Sets `self.background_texture`
|
||||
- Does not return a value
|
||||
|
||||
This means the method is hard to test in isolation because the real output is split across mutable instance state and SDL object creation.
|
||||
|
||||
## Functional Walkthrough
|
||||
|
||||
### 1. Initialization
|
||||
|
||||
The method creates:
|
||||
|
||||
- `texture_tiles`: a list of `(surface, x, y)` tuples for the static background
|
||||
- `self.cave_foreground_tiles`: a list of `(cell_x, cell_y, direction, surface, x, y)` tuples for overlay rendering
|
||||
- `half_cell`: used to place quarter-cell tiles at 20 px offsets when `cell_size` is 40
|
||||
|
||||
This immediately shows a hidden design choice: one map cell can emit up to four quarter tiles rather than a single full-tile sprite.
|
||||
|
||||
### 2. Cell iteration
|
||||
|
||||
The outer loop traverses every cell of `self.map.tiles`.
|
||||
|
||||
- `MAP_EMPTY`: skipped completely
|
||||
- `MAP_WALL`: potentially emits several quarter tiles
|
||||
- `MAP_TUNNEL`: emits cave overlays and sometimes grass filler tiles
|
||||
|
||||
### 3. Wall rendering logic
|
||||
|
||||
For `MAP_WALL`, the method evaluates the four quadrants independently.
|
||||
|
||||
#### Top-left quadrant
|
||||
|
||||
If the north-west corner is exposed, it chooses among:
|
||||
|
||||
- `inner_corners["WN"]`
|
||||
- `edges["W"]`
|
||||
- `edges["N"]`
|
||||
- `corners["NW"]`
|
||||
|
||||
based on whether the north and west neighbors are occupied.
|
||||
|
||||
#### Bottom-right quadrant
|
||||
|
||||
This is the densest branch. It checks south, east, and south-east occupancy.
|
||||
|
||||
- If all three are occupied, it usually draws a random grass tile.
|
||||
- With a 10% chance, it draws a flower instead, but only if the cell is not near the border and none of the neighboring cells involved are tunnels.
|
||||
- If only south or east are occupied, it chooses `inner_corners["ES"]`, `edges["E"]`, or `edges["S"]`.
|
||||
- Otherwise it uses `corners["SE"]`.
|
||||
|
||||
This branch mixes topology, decoration policy, border constraints, and tunnel suppression all in one nested block.
|
||||
|
||||
#### Top-right quadrant
|
||||
|
||||
Mirrors the top-left logic using north and east occupancy:
|
||||
|
||||
- `inner_corners["EN"]`
|
||||
- `edges["E"]`
|
||||
- `edges["N"]`
|
||||
- `corners["NE"]`
|
||||
|
||||
#### Bottom-left quadrant
|
||||
|
||||
Mirrors the same pattern using south and west occupancy:
|
||||
|
||||
- `inner_corners["WS"]`
|
||||
- `edges["W"]`
|
||||
- `edges["S"]`
|
||||
- `corners["SW"]`
|
||||
|
||||
### 4. Tunnel rendering logic
|
||||
|
||||
For `MAP_TUNNEL`, the method checks `above`, `below`, `left`, and `right` occupancy and chooses cave overlay sprites.
|
||||
|
||||
Observed behavior:
|
||||
|
||||
- If there is no occupied tile above, it always draws a grass filler in the bottom-right quarter and uses the `UP` cave sprite.
|
||||
- If there is an occupied tile above but not below, it uses the `DOWN` cave sprite.
|
||||
- If both above and below are occupied and the left side is blocked, it may use a full-quarter wall/flower texture in the cave list.
|
||||
- If both above and below are occupied and the left side is open, it draws a grass filler plus the `LEFT` cave sprite.
|
||||
- If above and below are occupied, left is blocked, and right is open, it uses the `RIGHT` cave sprite.
|
||||
|
||||
This logic appears tuned to the current level topology and asset set rather than representing a complete, explicit rule system for all tunnel neighbor combinations.
|
||||
|
||||
### 5. Commit phase
|
||||
|
||||
After traversal, the method calls `render_engine.create_texture(texture_tiles, fill_color=(128, 128, 128))` to compose one static SDL texture for the entire maze background.
|
||||
|
||||
This is the correct optimization boundary for the current architecture, but it also means SDL concerns leak directly into the generation logic.
|
||||
|
||||
## Why The Method Feels Complex
|
||||
|
||||
The complexity is not only “too many lines”. It comes from multiple kinds of coupling.
|
||||
|
||||
### 1. Mixed responsibilities
|
||||
|
||||
The method mixes:
|
||||
|
||||
- map analysis
|
||||
- rule selection
|
||||
- random decoration
|
||||
- cave overlay planning
|
||||
- final rendering commit
|
||||
|
||||
Each of these changes for different reasons, so they should not live in the same function.
|
||||
|
||||
### 2. Repeated neighborhood queries
|
||||
|
||||
Neighbor checks like `occupied(x, y - 1)` and `occupied(x + 1, y)` are recomputed many times, often inside overlapping branches. That makes the code noisy and increases the chance of introducing asymmetric bugs during edits.
|
||||
|
||||
### 3. Hidden representation mismatch
|
||||
|
||||
Background composition uses SDL surfaces, while cave overlays use textures. That is why the code has parallel helpers like `random_wall()` and `random_wall_texture()`. The behavior is valid, but the representation split is leaking into every branch.
|
||||
|
||||
### 4. Randomness is embedded in rule logic
|
||||
|
||||
The function directly calls global `random` during traversal. That makes visual behavior hard to snapshot-test or compare before and after a refactor.
|
||||
|
||||
### 5. Side effects are scattered across the class lifecycle
|
||||
|
||||
Invalidation is controlled elsewhere in `rats.py`, where the code manually clears:
|
||||
|
||||
- `self.background_texture`
|
||||
- `self.blood_layer_sprites`
|
||||
- `self.cave_foreground_tiles`
|
||||
|
||||
This is correct today, but it creates a fragile contract between game flow code and rendering code.
|
||||
|
||||
### 6. Tunnel rules are implicit
|
||||
|
||||
The tunnel branch contains nested assumptions that are hard to verify by inspection. It is not obvious whether the logic is exhaustive, map-specific, or intentionally asymmetric.
|
||||
|
||||
## Important Invariants To Preserve
|
||||
|
||||
Any refactor must keep these behaviors unless you explicitly choose to change them:
|
||||
|
||||
1. Blood stains remain outside the static background texture.
|
||||
2. `draw_cave_foreground()` must still be able to swap cave sprites for explosion sprites at runtime.
|
||||
3. Quarter-tile placement and offsets must remain visually identical.
|
||||
4. Random flower placement must preserve the current frequency and tunnel/border exclusions, or the change must be documented as a visual redesign.
|
||||
5. Theme asset selection must keep using surfaces for background composition and textures for runtime overlays unless the render-engine API changes.
|
||||
|
||||
## Refactor Goals
|
||||
|
||||
The target should be:
|
||||
|
||||
- easier to read
|
||||
- behaviorally stable
|
||||
- testable without SDL
|
||||
- explicit about map-topology rules
|
||||
- easy to extend with new wall or tunnel tile rules
|
||||
|
||||
## Recommended Refactor Direction
|
||||
|
||||
The safest path is not a full rewrite. It is a staged extraction toward a pure planning layer.
|
||||
|
||||
### Stage 1: Name The Concepts
|
||||
|
||||
Extract small private helpers without changing data structures yet.
|
||||
|
||||
Suggested helpers:
|
||||
|
||||
- `_is_occupied(x, y)`
|
||||
- `_is_tunnel(x, y)`
|
||||
- `_make_cell_context(x, y)`
|
||||
- `_append_background_tile(surface, x, y, texture_tiles)`
|
||||
- `_append_cave_tile(surface, x, y, direction)`
|
||||
- `_choose_wall_fill(x, y, allow_flower)`
|
||||
|
||||
This alone will remove repeated neighbor reads and make the current logic easier to reason about.
|
||||
|
||||
### Stage 2: Introduce A Pure Planning Model
|
||||
|
||||
Create lightweight data containers, for example:
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TilePlacement:
|
||||
surface: object
|
||||
x: int
|
||||
y: int
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CavePlacement:
|
||||
cell_x: int
|
||||
cell_y: int
|
||||
direction: str | None
|
||||
sprite: object
|
||||
x: int
|
||||
y: int
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CellContext:
|
||||
x: int
|
||||
y: int
|
||||
cell: int
|
||||
north: bool
|
||||
south: bool
|
||||
east: bool
|
||||
west: bool
|
||||
north_west: bool
|
||||
north_east: bool
|
||||
south_west: bool
|
||||
south_east: bool
|
||||
```
|
||||
|
||||
Then split the method into:
|
||||
|
||||
- `_build_background_plan()`
|
||||
- `_plan_wall_cell(context, plan)`
|
||||
- `_plan_tunnel_cell(context, plan)`
|
||||
- `_commit_background_plan(plan)`
|
||||
|
||||
The important shift is this: planning should produce plain Python data first, and SDL texture creation should happen only in the commit step.
|
||||
|
||||
### Stage 3: Replace Nested Branches With Rule Helpers
|
||||
|
||||
The wall logic is currently “four quadrants, each with a small rule tree”. Keep that structure, but make it explicit.
|
||||
|
||||
Suggested helpers:
|
||||
|
||||
- `_plan_wall_nw(context, px, py, plan)`
|
||||
- `_plan_wall_ne(context, px, py, half_cell, plan)`
|
||||
- `_plan_wall_sw(context, px, py, half_cell, plan)`
|
||||
- `_plan_wall_se(context, px, py, half_cell, plan)`
|
||||
|
||||
This sounds verbose, but it is much easier to review because each helper owns one visual quadrant and one set of rules.
|
||||
|
||||
### Stage 4: Isolate Decoration Policy
|
||||
|
||||
The flower rule is currently buried inside the `SE` branch. Extract it into a dedicated function such as:
|
||||
|
||||
```python
|
||||
def _should_place_flower(self, x, y, context) -> bool:
|
||||
...
|
||||
```
|
||||
|
||||
That function should own:
|
||||
|
||||
- the 10% probability
|
||||
- border exclusions
|
||||
- tunnel exclusions
|
||||
|
||||
This makes visual tuning possible without reopening the topology logic.
|
||||
|
||||
### Stage 5: Make Tunnel Rules Explicit
|
||||
|
||||
Tunnel behavior needs a named rule function with documented cases.
|
||||
|
||||
For example:
|
||||
|
||||
- `_classify_tunnel(context) -> TunnelPattern`
|
||||
- `_plan_tunnel_pattern(pattern, px, py, plan)`
|
||||
|
||||
Even if the final logic stays the same, naming the tunnel patterns will expose whether the code is intentionally map-specific or accidentally incomplete.
|
||||
|
||||
### Stage 6: Centralize Invalidation
|
||||
|
||||
Introduce one method such as:
|
||||
|
||||
```python
|
||||
def invalidate_background(self):
|
||||
self.background_texture = None
|
||||
self.cave_foreground_tiles.clear()
|
||||
```
|
||||
|
||||
Then use that method from lifecycle points in `rats.py`.
|
||||
|
||||
This reduces the chance of future bugs where one part of the cached rendering state is reset and another is forgotten.
|
||||
|
||||
## Suggested Final Shape
|
||||
|
||||
The long-term shape can stay inside `Graphics` and still be much cleaner:
|
||||
|
||||
```python
|
||||
def regenerate_background(self):
|
||||
plan = self._build_background_plan()
|
||||
self._commit_background_plan(plan)
|
||||
|
||||
def _build_background_plan(self):
|
||||
...
|
||||
|
||||
def _plan_wall_cell(self, context, plan):
|
||||
...
|
||||
|
||||
def _plan_tunnel_cell(self, context, plan):
|
||||
...
|
||||
|
||||
def _commit_background_plan(self, plan):
|
||||
self.cave_foreground_tiles = plan.cave_tiles
|
||||
self.background_texture = self.render_engine.create_texture(
|
||||
plan.background_tiles,
|
||||
fill_color=(128, 128, 128),
|
||||
)
|
||||
```
|
||||
|
||||
This would preserve the current class boundaries while making the core algorithm testable.
|
||||
|
||||
## Test Strategy Before Refactoring
|
||||
|
||||
Because the function is visual and randomized, refactoring without a guardrail is risky.
|
||||
|
||||
Recommended safety steps:
|
||||
|
||||
1. Introduce a seeded RNG path so map generation can be deterministic during tests.
|
||||
2. Add a small test map fixture that exercises walls, corners, borders, and tunnels.
|
||||
3. Snapshot the produced tile plan, not the SDL texture object.
|
||||
4. Verify cave overlay tuples are identical before and after the extraction.
|
||||
5. Add a smoke test for `draw_cave_foreground()` with an explosion unit to ensure cave sprite replacement still works.
|
||||
|
||||
## Proposed Implementation Order
|
||||
|
||||
### Phase 0: Freeze Current Behavior
|
||||
|
||||
- Add a deterministic RNG entry point or injectable random source.
|
||||
- Capture the current background plan for one or two representative maps.
|
||||
|
||||
### Phase 1: Extract Context And Emit Helpers
|
||||
|
||||
- Remove repeated `occupied(...)` calls.
|
||||
- Keep current tuple outputs and current SDL commit behavior.
|
||||
|
||||
### Phase 2: Split Wall And Tunnel Planning
|
||||
|
||||
- Move wall rules into quadrant helpers.
|
||||
- Move tunnel rules into a dedicated planner.
|
||||
|
||||
### Phase 3: Introduce A `BackgroundPlan`
|
||||
|
||||
- Return plain data from planning.
|
||||
- Keep SDL texture creation in one place.
|
||||
|
||||
### Phase 4: Centralize Cache Invalidation
|
||||
|
||||
- Replace direct state resets with a single background invalidation method.
|
||||
|
||||
### Phase 5: Optional Optimization Pass
|
||||
|
||||
- Consider caching immutable plans by `(level_index, theme_index)` if needed.
|
||||
- Consider precomputing per-cell contexts if profiling shows the planner is still hot.
|
||||
|
||||
## Refactor Risks And Questions
|
||||
|
||||
These should be clarified before implementation:
|
||||
|
||||
1. Are tunnel patterns guaranteed by the level data, or should the code become exhaustive for arbitrary maps?
|
||||
2. Is `occupied()` intentionally treating tunnels as “solid” for wall topology, or is that only a rendering shortcut?
|
||||
3. Is the flower placement rule part of the visual identity, or can it be simplified?
|
||||
4. Do we want to keep both surfaces and textures in the theme cache, or would a render-engine API change be acceptable later?
|
||||
|
||||
## Recommended First Refactor PR
|
||||
|
||||
The lowest-risk first PR would do only this:
|
||||
|
||||
1. Extract `CellContext` creation.
|
||||
2. Extract the four wall-quadrant planners.
|
||||
3. Extract tunnel planning into one helper.
|
||||
4. Leave the tuple formats and SDL commit step unchanged.
|
||||
|
||||
That PR would reduce complexity sharply while keeping the visual output almost certainly identical.
|
||||
|
||||
## Summary
|
||||
|
||||
`regenerate_background()` is complex because it is simultaneously a topology analyzer, decoration policy engine, cave overlay planner, and SDL background composer. The safest refactor is to separate planning from rendering, then isolate wall rules, tunnel rules, and decoration policy into named helpers with deterministic test coverage.
|
||||
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 388 B After Width: | Height: | Size: 257 B |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 399 B After Width: | Height: | Size: 279 B |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 399 B After Width: | Height: | Size: 296 B |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 380 B After Width: | Height: | Size: 243 B |
|
Before Width: | Height: | Size: 198 B |
|
Before Width: | Height: | Size: 354 B After Width: | Height: | Size: 174 B |
|
Before Width: | Height: | Size: 198 B |
|
Before Width: | Height: | Size: 354 B After Width: | Height: | Size: 189 B |
|
Before Width: | Height: | Size: 198 B |
|
Before Width: | Height: | Size: 354 B After Width: | Height: | Size: 184 B |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 419 B After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 425 B After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 428 B After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 416 B After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 464 B After Width: | Height: | Size: 400 B |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 443 B After Width: | Height: | Size: 402 B |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 401 B |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 458 B After Width: | Height: | Size: 390 B |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 436 B After Width: | Height: | Size: 374 B |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 457 B After Width: | Height: | Size: 414 B |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 457 B After Width: | Height: | Size: 383 B |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 449 B After Width: | Height: | Size: 423 B |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 392 B After Width: | Height: | Size: 473 B |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 392 B After Width: | Height: | Size: 325 B |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 392 B After Width: | Height: | Size: 332 B |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 391 B After Width: | Height: | Size: 325 B |
|
Before Width: | Height: | Size: 198 B |
|
Before Width: | Height: | Size: 356 B After Width: | Height: | Size: 197 B |
|
Before Width: | Height: | Size: 198 B |
|
Before Width: | Height: | Size: 356 B After Width: | Height: | Size: 197 B |
|
Before Width: | Height: | Size: 198 B |
|
Before Width: | Height: | Size: 355 B After Width: | Height: | Size: 193 B |
|
Before Width: | Height: | Size: 198 B |
|
Before Width: | Height: | Size: 351 B After Width: | Height: | Size: 171 B |
|
Before Width: | Height: | Size: 198 B |
|
Before Width: | Height: | Size: 358 B After Width: | Height: | Size: 194 B |
|
Before Width: | Height: | Size: 198 B |
|
Before Width: | Height: | Size: 358 B After Width: | Height: | Size: 191 B |
|
Before Width: | Height: | Size: 198 B |
|
Before Width: | Height: | Size: 355 B After Width: | Height: | Size: 187 B |
|
Before Width: | Height: | Size: 198 B |
|
Before Width: | Height: | Size: 356 B After Width: | Height: | Size: 187 B |
|
Before Width: | Height: | Size: 198 B |
|
Before Width: | Height: | Size: 352 B After Width: | Height: | Size: 187 B |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 380 B After Width: | Height: | Size: 242 B |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 390 B After Width: | Height: | Size: 259 B |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 393 B After Width: | Height: | Size: 286 B |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 377 B After Width: | Height: | Size: 243 B |
|
Before Width: | Height: | Size: 198 B |
|
Before Width: | Height: | Size: 355 B After Width: | Height: | Size: 175 B |
|
Before Width: | Height: | Size: 198 B |
|
Before Width: | Height: | Size: 354 B After Width: | Height: | Size: 189 B |
|
Before Width: | Height: | Size: 198 B |
|
Before Width: | Height: | Size: 354 B After Width: | Height: | Size: 184 B |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 409 B After Width: | Height: | Size: 306 B |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 407 B After Width: | Height: | Size: 312 B |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 402 B After Width: | Height: | Size: 277 B |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 405 B After Width: | Height: | Size: 281 B |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 444 B After Width: | Height: | Size: 399 B |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 439 B After Width: | Height: | Size: 389 B |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 441 B After Width: | Height: | Size: 387 B |
|
Before Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 453 B After Width: | Height: | Size: 399 B |
|
Before Width: | Height: | Size: 358 B |