Aggiorna e approfondisci tutta la documentazione
Riscritti tutti i file doc/ e README.md per riflettere lo stato attuale del gioco (8 livelli, multi-nemico, corsa, finale tragico, musica dedicata, fog scalabile, flush dinamico, claimed.png death screen, ecc.): - index.md: nuovo indice con riferimenti ai 7 capitoli aggiornati. - architecture.md: tabella moduli con righe, variabili chiave globals, pipeline asset, vincoli hardware (ROM/WRAM/VRAM/OAM). - graphics.md: proiezione iso, fog scalabile (fog_radius), auto-tiling multi-pass, flush dinamico 16-righe con wrap, HUD stamina + livello. - generation.md: DFS con stack WRAM, dimensioni crescenti 7->21, botola a distanza scalabile, spawn multi-nemico. - gameplay.md: DAS, camminata, corsa, salto, stamina scalabile, progressione 8 livelli, schermate (death/going deeper/finale). - ai.md: multi-entity (8 fantasmi), greedy, cooldown scalabile, hitbox, culling. - audio.md: 4 canali APU, 5 tracce (title 112/3canali, gameplay 96, gameover 128, finale 192 loop, going deeper 96), SFX. - AScreamFromTheDark_report.md: riscritto completamente (~200 righe), tutte le sezioni aggiornate, workaround storici, debiti tecnici. - README.md: intro, caratteristiche, soundtrack, architettura, test.
This commit is contained in:
@@ -1,29 +1,61 @@
|
||||
# Intelligenza Artificiale (Il Fantasma)
|
||||
# Intelligenza Artificiale (Fantasmi)
|
||||
|
||||
Il gioco utilizza un inseguitore, soprannominato "Il Fantasma", che bracca il giocatore all'interno del labirinto. Poiché il giocatore si muove velocemente, l'AI del fantasma richiede limiti artificiali per risultare un nemico superabile e bilanciato.
|
||||
## Multi-Entity (fino a 8 fantasmi)
|
||||
|
||||
## Pathfinding: Perché "Greedy" e non A*?
|
||||
Il gioco supporta fino a `MAX_ENEMIES = 8` fantasmi coesistenti. Il numero attivo è `num_enemies = level` (1 al livello 1, 8 al livello 8). Lo stato di ciascun fantasma è in **array indicizzati**:
|
||||
|
||||
Sui sistemi moderni o su griglie piccole, l'A-Star (A*) è l'algoritmo standard per il pathfinding dei nemici. Calcola il percorso perfetto evitando vicoli ciechi.
|
||||
Sul Game Boy (CPU 4 MHz), gestire la memoria (heap, nodi) richiesta da A* per un array 7x7 genererebbe ritardi nei frame (frame drops) inammissibili per un gioco real-time.
|
||||
|
||||
La soluzione implementata è un approccio **Greedy (Goloso)**:
|
||||
Ad ogni passo, il fantasma verifica le 4 celle a lui adiacenti (Nord, Sud, Est, Ovest). Tra quelle calpestabili (non muri), sceglie quella la cui coordinata rende **minima la distanza spaziale al quadrato verso il giocatore** (`dx^2 + dy^2`).
|
||||
Non usiamo la radice quadrata (necessaria per calcolare la distanza esatta euclidea) perché troppo costosa per la CPU; dal momento che stiamo solo comparando quale distanza sia minore, i quadrati bastano e avanzano matematicamente (`A < B <=> A^2 < B^2`).
|
||||
|
||||
Un algoritmo Greedy tende a incastrarsi in ostacoli a forma di U, ma essendo questo un labirinto dinamico pieno di loop a corridoio singolo, questo "difetto" si trasforma in una dinamica di gioco: i giocatori astuti possono sfruttare il level design per confondere l'AI e aggirarla.
|
||||
|
||||
## Attivazione e Cooldown
|
||||
|
||||
Il fantasma non parte immediatamente alla carica. Usa la distanza di Chebyshev per "vedere":
|
||||
```c
|
||||
if (dist <= 2) { // Inizia a muoversi }
|
||||
uint8_t enemy_lx[8], enemy_ly[8];
|
||||
uint8_t enemy_is_moving[8], enemy_move_progress[8];
|
||||
int8_t enemy_start_lx[8], enemy_start_ly[8];
|
||||
int8_t enemy_target_lx[8], enemy_target_ly[8];
|
||||
int16_t enemy_start_px[8], enemy_start_py[8];
|
||||
int16_t enemy_target_px[8], enemy_target_py[8];
|
||||
uint8_t enemy_cooldown[8];
|
||||
```
|
||||
Ovvero, inizia l'inseguimento solo quando entra nel riquadro visivo del giocatore (5x5, distanza <= 2).
|
||||
|
||||
Inoltre, il fantasma impiega 16 frame per muoversi da un blocco all'altro (come il player), ma è limitato dalla variabile `enemy_cooldown = 60`. Dopo ogni singolo passo, rimane congelato per 1 secondo intero prima di poterne fare un altro. Questo garantisce che il fantasma sia più lento del giocatore e ti costringe, qualora commettessi un errore, a usare rapidamente la meccanica di Salto Evasivo e seminarlo.
|
||||
Ogni fantasma usa 2 slot OAM a partire da `2 + i*2` (il player usa 0-1). Tutti condividono la stessa tile data (ghost sprite), con palette invertita (OBP1 = 0x1B, bianco su scuro).
|
||||
|
||||
## Pathfinding: Greedy invece di A*
|
||||
|
||||
A* richiederebbe heap, nodi e calcoli pesanti — inammissibile su 4 MHz, soprattutto con 8 fantasmi. L'approccio **Greedy** è leggero:
|
||||
|
||||
Per ciascun fantasma, tra le 4 celle adiacenti calpestabili, sceglie quella che **minimizza la distanza al quadrato** verso il giocatore (`dx² + dy²`). No sqrt (i quadrati bastano per il confronto `A < B ⇔ A² < B²`).
|
||||
|
||||
**Difetto voluto**: il Greedy si incastra nei vicoli a U. In un labirinto con loop, questo diventa una dinamica di gioco — il giocatore astuto sfrutta il level design per seminare i fantasmi.
|
||||
|
||||
## Attivazione e Cooldown Scalabile
|
||||
|
||||
### Attivazione
|
||||
Il fantasma insegue solo se entro il raggio di nebbia:
|
||||
```c
|
||||
if (dist <= fog_radius) { // inizia a inseguire }
|
||||
```
|
||||
`fog_radius` = 2 (5×5) ai livelli 1-6, = 1 (3×3) ai livelli 7-8.
|
||||
|
||||
### Cooldown
|
||||
Dopo ogni passo (16 frame di LERP), il fantasma aspetta `enemy_step_cooldown` frame:
|
||||
- Livello 1: 60 frame (1 secondo) — lento, gestibile.
|
||||
- Livello 8: 11 frame — implacabile, quasi continuo.
|
||||
|
||||
I cooldown iniziali sono **sfasati** (`enemy_step_cooldown + i*8`) così i fantasmi non si muovono in sincrono.
|
||||
|
||||
## Hitbox Pixel-Perfect
|
||||
|
||||
Nonostante la mappa sia calcolata su una pura griglia logica bidimensionale (1 passo = 1 casella X,Y), la condizione di "Sconfitta" non si basa sul fatto che il giocatore e il fantasma occupino la medesima casella logica contemporaneamente. Si controlla invece che le coordinate fisiche (i veri e propri **pixel** renderizzati sullo schermo del Game Boy) si sovrappongano con un margine di errore basso (12 pixel in orizzontale e 6 in verticale).
|
||||
Questo rende la morte "giusta" agli occhi dell'utente e permette scambi per il rotto della cuffia!
|
||||
La morte non si basa sulla coincidenza di cella logica, ma sulla **sovrapposizione dei pixel fisici** renderizzati:
|
||||
|
||||
```c
|
||||
if (|p_px - enemy_px| < 12 && |p_py - enemy_py| < 6) {
|
||||
game_over = 1; // sconfitta
|
||||
}
|
||||
```
|
||||
|
||||
Questo rende la morte "giusta" agli occhi del giocatore e permette scambi per il rotto della cuffia. Al primo fantasma che catcha il player, il loop si ferma (`return`).
|
||||
|
||||
## Culling
|
||||
|
||||
I fantasmi sono renderizzati solo se:
|
||||
1. Entro `fog_radius` dal giocatore (Chebyshev).
|
||||
2. On-screen (con wrap `& 255` + soglie fisiche `-8..168` / `-8..152`).
|
||||
|
||||
Altrimenti lo sprite è spostato offscreen `(0,0)`.
|
||||
Reference in New Issue
Block a user