Grow maze size with level (7x7 -> 17x17, +2 per level)
The maze side length is now a runtime global 'map_size' (MAP_SIZE=7 at level 1, +2 per level, capped at MAX_MAP_SIZE=17), while the maze array is allocated with the MAX_MAP_SIZE bound. All modules (maze DFS, fog rendering, player/enemy bounds, hatch and enemy-spawn placement) now use map_size at runtime. - globals: maze[MAX_MAP_SIZE][MAX_MAP_SIZE], map_size global, MAX_MAP_SIZE=17. - maze.c: DFS on map_size; stack/valid arrays made static (WRAM) and sized for the max maze to avoid hardware-stack overflow; hatch min distance scales with map_size/2. - engine.c: sets map_size from level before generate_maze; enemy spawn min distance scales with map_size/2. - render.c: flush the full 32x32 background map (the old rows 2-17 optimization broke for large mazes where the 5x5 fog window wraps outside that range); dynamic bounds; signed/unsigned casts. - player_logic/enemy_logic: dynamic map_size bounds. Verified via PyBoy: sizes 7,9,11,13,15,17,17 across levels 1-7, each with a hatch; rendering and movement intact at 17x17 and at 7x7.
This commit is contained in:
@@ -13,7 +13,7 @@ Un survival-horror procedurale in prospettiva isometrica per Game Boy (DMG/CGB),
|
|||||||
- **Delayed Auto-Shift (DAS)**: controlli alla Tetris — delay iniziale di 12 frame e ripetizione ogni 6 frame per il movimento continuo tenendo premuto il D-Pad.
|
- **Delayed Auto-Shift (DAS)**: controlli alla Tetris — delay iniziale di 12 frame e ripetizione ogni 6 frame per il movimento continuo tenendo premuto il D-Pad.
|
||||||
- **Salto evasivo con Stamina**: A+direzione scavalca il blocco adiacente atterrando 2 tile più in là (la cella intermedia deve essere un muro). Costa 60 punti stamina; la barra si ricarica di 1 punto al secondo.
|
- **Salto evasivo con Stamina**: A+direzione scavalca il blocco adiacente atterrando 2 tile più in là (la cella intermedia deve essere un muro). Costa 60 punti stamina; la barra si ricarica di 1 punto al secondo.
|
||||||
- **Corsa con Stamina**: B+direzione fa correre il protagonista: il passo dura 8 frame invece di 16 e consuma 10 stamina per tile, con DAS più rapido per incatenare i tile fluidamente. Se la stamina scende sotto 10 ripiega automaticamente sulla camminata normale.
|
- **Corsa con Stamina**: B+direzione fa correre il protagonista: il passo dura 8 frame invece di 16 e consuma 10 stamina per tile, con DAS più rapido per incatenare i tile fluidamente. Se la stamina scende sotto 10 ripiega automaticamente sulla camminata normale.
|
||||||
- **Progressione livelli**: si parte dal livello 1; raggiungere la botola fa sprofondare nel livello successivo (livello +1). L'indicatore `L<n>` in alto a sinistra (sprite HUD, come la barra stamina in alto a destra) mostra il livello corrente e si aggiorna ad ogni livello superato. In caso di sconfitta si ricomincia dall'**ultimo livello raggiunto** (il contatore non si azzera).
|
- **Progressione livelli**: si parte dal livello 1; raggiungere la botola fa sprofondare nel livello successivo (livello +1). L'indicatore `L<n>` in alto a sinistra (sprite HUD, come la barra stamina in alto a destra) mostra il livello corrente e si aggiorna ad ogni livello superato. In caso di sconfitta si ricomincia dall'**ultimo livello raggiunto** (il contatore non si azzera). **La dimensione del labirinto cresce col livello**: +2 tile per lato ad ogni livello (7→9→11→13→15→17), capped a 17x17, così la difficoltà aumenta progressivamente.
|
||||||
- **AI del fantasma**: pathfinding greedy con distanza al quadrato (niente sqrt, niente A*), cooldown di 1 secondo tra i passi, hitbox pixel-perfect (12×6 px) per una morte "giusta".
|
- **AI del fantasma**: pathfinding greedy con distanza al quadrato (niente sqrt, niente A*), cooldown di 1 secondo tra i passi, hitbox pixel-perfect (12×6 px) per una morte "giusta".
|
||||||
- **Audio procedurale**: colonna sonora sintetizzata manipolando direttamente i registri APU via VBL interrupt (nessun campione).
|
- **Audio procedurale**: colonna sonora sintetizzata manipolando direttamente i registri APU via VBL interrupt (nessun campione).
|
||||||
- **Schermate a tutto schermo**: copertina 2-bit nativa per il titolo, immagine "Going Deeper" per la vittoria, metasprite "GAME OVER" per la sconfitta.
|
- **Schermate a tutto schermo**: copertina 2-bit nativa per il titolo, immagine "Going Deeper" per la vittoria, metasprite "GAME OVER" per la sconfitta.
|
||||||
|
|||||||
@@ -56,9 +56,10 @@ py = (lx + ly) * 8 + 16
|
|||||||
Camera centrata: `scroll_x = px - 64`, `scroll_y = py - 72` (160×144 display → centro 80×72, con offset hardware).
|
Camera centrata: `scroll_x = px - 64`, `scroll_y = py - 72` (160×144 display → centro 80×72, con offset hardware).
|
||||||
|
|
||||||
### 3.2 Generazione del labirinto (`maze.c`)
|
### 3.2 Generazione del labirinto (`maze.c`)
|
||||||
- **DFS iterativo con stack in WRAM** (`stack_x[49]/stack_y[49]`): evita l'overflow dello stack hardware del LR35902 ricorrendo a un array in RAM. Celle dispari = stanze, celle pari = muri divisori; scava il muro di mezzo con la media aritmetica delle coordinate.
|
- **DFS iterativo con stack in WRAM** (`stack_x/y` statici, sized `(MAX_MAP_SIZE/2)^2`): evita l'overflow dello stack hardware del LR35902. Celle dispari = stanze, celle pari = muri divisori; scava il muro di mezzo con la media aritmetica delle coordinate.
|
||||||
- **Fase 2 — loop**: riapre muri residui che collegano due corridoi opposti con probabilità 15%. Vitale per il gameplay d'inseguimento (permette di aggirare il fantasma).
|
- **Dimensione crescente col livello**: `map_size = MAP_SIZE + 2*(level-1)`, capped a `MAX_MAP_SIZE`=17 (sempre dispari). L'array `maze` è allocato `[MAX_MAP_SIZE][MAX_MAP_SIZE]` (289 byte) e i moduli usano `map_size` come bound runtime.
|
||||||
- **Fase 3 — botola**: sceglie una cella calpestabile **a sufficiente distanza** dalla partenza `(1,1)` (distanza di Chebyshev ≥ `MIN_GOAL_DIST` = 3) tra **tutte** le celle del labirinto, non più vincolata al bordo sud. Fallback estremo sulla cella calpestabile più lontana in assoluto da `(1,1)` se nessuna cella fosse a sufficienza distante (teoricamente impossibile in un perfect maze 7x7).
|
- **Fase 2 — loop**: riapre muri residui che collegano due corridoi opposti con probabilità 15%. Vitale per il gameplay d'inseguimento.
|
||||||
|
- **Fase 3 — botola**: sceglie una cella calpestabile a distanza di Chebyshev ≥ `map_size/2` dalla partenza `(1,1)` (soglia scalata: 3 per 7x7, 8 per 17x17), tra **tutte** le celle del labirinto. Fallback sulla cella più lontana in assoluto.
|
||||||
|
|
||||||
### 3.3 Movimento & DAS (`player_logic.c`)
|
### 3.3 Movimento & DAS (`player_logic.c`)
|
||||||
- **State machine rigida**: durante `is_moving` (16 frame di LERP, o 8 in corsa) ogni input è ignorato → movimento strettamente grid-based stile Zelda/Pokémon.
|
- **State machine rigida**: durante `is_moving` (16 frame di LERP, o 8 in corsa) ogni input è ignorato → movimento strettamente grid-based stile Zelda/Pokémon.
|
||||||
@@ -87,7 +88,7 @@ Distanza di **Chebyshev** `max(|dx|,|dy|)` invece di euclidea (no sqrt, no looku
|
|||||||
- Questo evita l'uso di sprite hardware per la botola (che causerebbero flickering per il limite di 10 sprite/scanline) e contiene il consumo di VRAM (limite 256 tile).
|
- Questo evita l'uso di sprite hardware per la botola (che causerebbero flickering per il limite di 10 sprite/scanline) e contiene il consumo di VRAM (limite 256 tile).
|
||||||
|
|
||||||
### 3.6 Ottimizzazione rendering
|
### 3.6 Ottimizzazione rendering
|
||||||
Trasferisce solo le 16 righe visibili invece di tutte le 32: `set_bkg_tiles(0, 2, 32, 16, &map_buffer[2*32])`. Aggiornamento a `move_progress==8` (metà passo) per fluidità del fog of war. `map_buffer` usato con wrap `& 31` per gestire lo scroll modulare.
|
`memset` azzera `map_buffer` (32x32) ad ogni `draw_map`, poi disegna solo la finestra fog 5x5 (~25 tile). Trasferisce l'intera mappa 32x32 al background hardware (`set_bkg_tiles(0,0,32,32,map_buffer)`): necessario perché con labirinti grandi (`map_size` > 7) la finestra fog, proiettata in coordinate iso assolute, può cadere fuori dal range di righe che la vecchia ottimizzazione (righe 2-17) flussava, a causa del wrapping `& 31`. `draw_map` è chiamato solo ai passi del movimento (frame 8 e completamento, non ogni frame), quindi il flush completo è sostenibile. Aggiornamento a `move_progress==8` (metà passo) per fluidità del fog of war. `map_buffer` usato con wrap `& 31` per gestire lo scroll modulare.
|
||||||
|
|
||||||
### 3.7 AI del fantasma (`enemy_logic.c`)
|
### 3.7 AI del fantasma (`enemy_logic.c`)
|
||||||
- **Greedy invece di A***: confronta la distanza al quadrato `dx²+dy²` delle 4 celle adiacenti calpestabili e sceglie la minima. No sqrt (pesante su 4 MHz), no heap/nodi (frame drops inammissibili). Difetto voluto: si incastra in vicoli a U → diventa una dinamica di gameplay (seminarlo col level design).
|
- **Greedy invece di A***: confronta la distanza al quadrato `dx²+dy²` delle 4 celle adiacenti calpestabili e sceglie la minima. No sqrt (pesante su 4 MHz), no heap/nodi (frame drops inammissibili). Difetto voluto: si incastra in vicoli a U → diventa una dinamica di gameplay (seminarlo col level design).
|
||||||
|
|||||||
@@ -39,4 +39,10 @@ Se la Stamina scende sotto 10, B+direzione **ripiega silenziosamente su camminat
|
|||||||
|
|
||||||
Il gioco parte dal **livello 1**. Raggiungere la botola (casella traguardo) significa "sprofondare più giù" (*Going Deeper*): alla pressione di START il livello viene **incrementato** e viene generato un nuovo labirinto. In caso di sconfitta (cattura da parte del fantasma) si **ricomincia dallo stesso livello raggiunto**: il contatore non si azzera, solo il passaggio per il titolo (nuova partita) riparte da 1.
|
Il gioco parte dal **livello 1**. Raggiungere la botola (casella traguardo) significa "sprofondare più giù" (*Going Deeper*): alla pressione di START il livello viene **incrementato** e viene generato un nuovo labirinto. In caso di sconfitta (cattura da parte del fantasma) si **ricomincia dallo stesso livello raggiunto**: il contatore non si azzera, solo il passaggio per il titolo (nuova partita) riparte da 1.
|
||||||
|
|
||||||
|
## Dimensione crescente del labirinto
|
||||||
|
|
||||||
|
Ad ogni livello la **dimensione del labirinto cresce di 2 tile per lato**: si parte da 7x7 (livello 1) e si arriva a 9x9, 11x11, 13x13, 15x15, fino al cap di **17x17**. La dimensione corrente è la variabile globale `map_size` (l'array `maze` è allocato con bound `MAX_MAP_SIZE` = 17 in modo da contenere qualunque dimensione). Il DFS in `maze.c` genera un perfect maze su `map_size`x`map_size` (le celle dispari sono le stanze, per questo `map_size` è sempre dispari), poi lo rompe con i loop al 15% e posiziona la botola a distanza di Chebyshev ≥ `map_size/2` dalla partenza. Anche lo spawn del fantasma scala: parte a distanza ≥ `map_size/2`, così nelle mappe grandi non si attiva subito (il suo cono visivo resta comunque 5x5, fissato dal fog of war).
|
||||||
|
|
||||||
|
Poiché con labirinti grandi la finestra fog-of-war 5x5, proiettata in coordinate isometriche assolute, può cadere fuori dal vecchio range di righe flussato (2-17) a causa del wrapping della mappa 32x32, `draw_map` ora flussa l'intera mappa 32x32 al hardware background. `draw_map` è chiamato solo ai passi del movimento (non ogni frame), quindi il costo è sostenibile.
|
||||||
|
|
||||||
L'indicatore `L<n>` è mostrato in **alto a sinistra** tramite tre sprite hardware (OAM ID 23–25), disegnati dall'asset `level.png` (11 glifi 8x16: 'L', '0'–'9'). Le decine vengono mostrate solo dal livello 10 in poi (sotto, lo sprite decine è spostato fuori schermo). Come la barra stamina (in alto a destra), l'indicatore è a coordinate-schermo fisse e indipendente dallo scroll isometrico, e viene nascosto durante il game over e sul titolo. La base VRAM dei glifi è allineata a un indice **pari** perché in modalità sprite 8x16 l'hardware ignora il bit meno significativo dell'indice tile.
|
L'indicatore `L<n>` è mostrato in **alto a sinistra** tramite tre sprite hardware (OAM ID 23–25), disegnati dall'asset `level.png` (11 glifi 8x16: 'L', '0'–'9'). Le decine vengono mostrate solo dal livello 10 in poi (sotto, lo sprite decine è spostato fuori schermo). Come la barra stamina (in alto a destra), l'indicatore è a coordinate-schermo fisse e indipendente dallo scroll isometrico, e viene nascosto durante il game over e sul titolo. La base VRAM dei glifi è allineata a un indice **pari** perché in modalità sprite 8x16 l'hardware ignora il bit meno significativo dell'indice tile.
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Algoritmo Depth-First Search (DFS)
|
## Algoritmo Depth-First Search (DFS)
|
||||||
|
|
||||||
Per garantire che ogni sessione di gioco sia unica, il labirinto viene generato proceduralmente ad ogni avvio (usando `DIV_REG` hardware del Game Boy come seed pseudo-casuale).
|
Per garantire che ogni sessione di gioco sia unica, il labirinto viene generato proceduralmente ad ogni avvio (usando `DIV_REG` hardware del Game Boy come seed pseudo-casuale). La **dimensione del labirinto cresce col livello**: la variabile globale `map_size` parte da `MAP_SIZE` (7) al livello 1 e aumenta di 2 per livello fino al cap `MAX_MAP_SIZE` (17) — quindi 7x7, 9x9, 11x11, 13x13, 15x15, 17x17. `map_size` è sempre dispari perchè il DFS usa le celle dispari come stanze e le pari come muri divisori.
|
||||||
|
|
||||||
L'algoritmo utilizzato in `maze.c` è un **Depth-First Search (DFS)** con Backtracking, modellato per creare un "Perfect Maze" (labirinto perfetto in cui ogni cella è raggiungibile tramite un unico percorso possibile e senza cicli).
|
L'algoritmo utilizzato in `maze.c` è un **Depth-First Search (DFS)** con Backtracking, modellato per creare un "Perfect Maze" (labirinto perfetto in cui ogni cella è raggiungibile tramite un unico percorso possibile e senza cicli).
|
||||||
La mappa è concettualmente divisa in:
|
La mappa è concettualmente divisa in:
|
||||||
|
|||||||
+2
-2
@@ -65,14 +65,14 @@ void update_enemy_logic(void) {
|
|||||||
int16_t min_dist_sq = (int16_t)dx * dx + (int16_t)dy * dy;
|
int16_t min_dist_sq = (int16_t)dx * dx + (int16_t)dy * dy;
|
||||||
|
|
||||||
// Controllo 1: Direzione Down-Right (+1 X)
|
// Controllo 1: Direzione Down-Right (+1 X)
|
||||||
if (enemy_lx + 1 < MAP_SIZE && maze[enemy_ly][enemy_lx + 1] != 0) {
|
if (enemy_lx + 1 < map_size && maze[enemy_ly][enemy_lx + 1] != 0) {
|
||||||
int8_t ndx = (int8_t)player_lx - (int8_t)(enemy_lx + 1);
|
int8_t ndx = (int8_t)player_lx - (int8_t)(enemy_lx + 1);
|
||||||
int8_t ndy = (int8_t)player_ly - (int8_t)enemy_ly;
|
int8_t ndy = (int8_t)player_ly - (int8_t)enemy_ly;
|
||||||
int16_t d_sq = (int16_t)ndx * ndx + (int16_t)ndy * ndy;
|
int16_t d_sq = (int16_t)ndx * ndx + (int16_t)ndy * ndy;
|
||||||
if (d_sq < min_dist_sq) { min_dist_sq = d_sq; best_nx = enemy_lx + 1; best_ny = enemy_ly; }
|
if (d_sq < min_dist_sq) { min_dist_sq = d_sq; best_nx = enemy_lx + 1; best_ny = enemy_ly; }
|
||||||
}
|
}
|
||||||
// Controllo 2: Direzione Down-Left (+1 Y)
|
// Controllo 2: Direzione Down-Left (+1 Y)
|
||||||
if (enemy_ly + 1 < MAP_SIZE && maze[enemy_ly + 1][enemy_lx] != 0) {
|
if (enemy_ly + 1 < map_size && maze[enemy_ly + 1][enemy_lx] != 0) {
|
||||||
int8_t ndx = (int8_t)player_lx - (int8_t)enemy_lx;
|
int8_t ndx = (int8_t)player_lx - (int8_t)enemy_lx;
|
||||||
int8_t ndy = (int8_t)player_ly - (int8_t)(enemy_ly + 1);
|
int8_t ndy = (int8_t)player_ly - (int8_t)(enemy_ly + 1);
|
||||||
int16_t d_sq = (int16_t)ndx * ndx + (int16_t)ndy * ndy;
|
int16_t d_sq = (int16_t)ndx * ndx + (int16_t)ndy * ndy;
|
||||||
|
|||||||
+16
-7
@@ -81,7 +81,12 @@ void engine_init(void) {
|
|||||||
|
|
||||||
// Usa il registro divisore hardware (DIV_REG) per seedare l'RNG. Garantisce labirinti diversi ogni volta.
|
// Usa il registro divisore hardware (DIV_REG) per seedare l'RNG. Garantisce labirinti diversi ogni volta.
|
||||||
initrand(DIV_REG);
|
initrand(DIV_REG);
|
||||||
|
|
||||||
|
// Dimensione del labirinto crescente col livello: +2 tile per livello a partire da 7,
|
||||||
|
// capped a MAX_MAP_SIZE (17). Sempre dispari (per il pattern stanza/muro del DFS).
|
||||||
|
map_size = MAP_SIZE + 2 * (level - 1);
|
||||||
|
if (map_size > MAX_MAP_SIZE) map_size = MAX_MAP_SIZE;
|
||||||
|
|
||||||
// DELEGA LA GENERAZIONE: Chiede al modulo maze.c di creare l'array della mappa
|
// DELEGA LA GENERAZIONE: Chiede al modulo maze.c di creare l'array della mappa
|
||||||
generate_maze();
|
generate_maze();
|
||||||
|
|
||||||
@@ -106,8 +111,8 @@ void engine_init(void) {
|
|||||||
// Fallback di sicurezza: Cerca la prima casella libera se 1,1 è occupato (teoricamente impossibile col DFS)
|
// Fallback di sicurezza: Cerca la prima casella libera se 1,1 è occupato (teoricamente impossibile col DFS)
|
||||||
if (maze[player_ly][player_lx] == 0) {
|
if (maze[player_ly][player_lx] == 0) {
|
||||||
uint8_t found = 0;
|
uint8_t found = 0;
|
||||||
for (uint8_t y = 1; y < MAP_SIZE - 1; y++) {
|
for (uint8_t y = 1; y < map_size - 1; y++) {
|
||||||
for (uint8_t x = 1; x < MAP_SIZE - 1; x++) {
|
for (uint8_t x = 1; x < map_size - 1; x++) {
|
||||||
if (maze[y][x] == 1) {
|
if (maze[y][x] == 1) {
|
||||||
player_lx = x;
|
player_lx = x;
|
||||||
player_ly = y;
|
player_ly = y;
|
||||||
@@ -120,17 +125,21 @@ void engine_init(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SPAWN DEL NEMICO
|
// SPAWN DEL NEMICO
|
||||||
// Cerca randomicamente un punto lontano almeno 3 celle dal giocatore
|
// Cerca randomicamente un punto lontano almeno map_size/2 celle dal giocatore
|
||||||
|
// (scala con la dimensione del labirinto: in una mappa grande il fantasma parte
|
||||||
|
// lontano e non si attiva finché non entra nel cono visivo 5x5 del fog of war).
|
||||||
|
uint8_t enemy_min_dist = map_size / 2;
|
||||||
|
if (enemy_min_dist < 3) enemy_min_dist = 3;
|
||||||
while (1) {
|
while (1) {
|
||||||
uint8_t rx = rand() % MAP_SIZE;
|
uint8_t rx = rand() % map_size;
|
||||||
uint8_t ry = rand() % MAP_SIZE;
|
uint8_t ry = rand() % map_size;
|
||||||
if (maze[ry][rx] == 1) {
|
if (maze[ry][rx] == 1) {
|
||||||
int8_t dx = (int8_t)rx - (int8_t)player_lx;
|
int8_t dx = (int8_t)rx - (int8_t)player_lx;
|
||||||
int8_t dy = (int8_t)ry - (int8_t)player_ly;
|
int8_t dy = (int8_t)ry - (int8_t)player_ly;
|
||||||
if (dx < 0) dx = -dx;
|
if (dx < 0) dx = -dx;
|
||||||
if (dy < 0) dy = -dy;
|
if (dy < 0) dy = -dy;
|
||||||
int8_t dist = (dx > dy) ? dx : dy;
|
int8_t dist = (dx > dy) ? dx : dy;
|
||||||
if (dist >= 3) {
|
if (dist >= (int8_t)enemy_min_dist) {
|
||||||
enemy_lx = rx;
|
enemy_lx = rx;
|
||||||
enemy_ly = ry;
|
enemy_ly = ry;
|
||||||
break;
|
break;
|
||||||
|
|||||||
+2
-1
@@ -4,7 +4,8 @@
|
|||||||
volatile uint8_t game_over = 0;
|
volatile uint8_t game_over = 0;
|
||||||
volatile uint8_t game_over_timer = 0;
|
volatile uint8_t game_over_timer = 0;
|
||||||
|
|
||||||
uint8_t maze[MAP_SIZE][MAP_SIZE];
|
uint8_t map_size = MAP_SIZE;
|
||||||
|
uint8_t maze[MAX_MAP_SIZE][MAX_MAP_SIZE];
|
||||||
uint8_t map_buffer[32 * 32];
|
uint8_t map_buffer[32 * 32];
|
||||||
|
|
||||||
uint8_t player_lx = 1;
|
uint8_t player_lx = 1;
|
||||||
|
|||||||
+7
-7
@@ -22,9 +22,11 @@ extern volatile uint8_t game_over;
|
|||||||
extern volatile uint8_t game_over_timer;
|
extern volatile uint8_t game_over_timer;
|
||||||
|
|
||||||
// --- Map Data ---
|
// --- Map Data ---
|
||||||
#define MAP_SIZE 7
|
#define MAP_SIZE 7 // initial maze size (level 1); must be odd
|
||||||
// The generated maze: 0 = Wall, 1 = Floor, 2 = Victory Tile
|
#define MAX_MAP_SIZE 17 // hard cap for maze growth (odd); used as the array bound
|
||||||
extern uint8_t maze[MAP_SIZE][MAP_SIZE];
|
// The generated maze: 0 = Wall, 1 = Floor, 2 = Victory Tile (hatch)
|
||||||
|
extern uint8_t map_size; // current maze side length (grows with level, MAP_SIZE..MAX_MAP_SIZE)
|
||||||
|
extern uint8_t maze[MAX_MAP_SIZE][MAX_MAP_SIZE];
|
||||||
|
|
||||||
// --- Camera & Rendering ---
|
// --- Camera & Rendering ---
|
||||||
// Map buffer used to draw the isometric tiles into the Game Boy Background map
|
// Map buffer used to draw the isometric tiles into the Game Boy Background map
|
||||||
@@ -36,9 +38,7 @@ extern int8_t scroll_y;
|
|||||||
extern uint8_t stairs_lx;
|
extern uint8_t stairs_lx;
|
||||||
extern uint8_t stairs_ly;
|
extern uint8_t stairs_ly;
|
||||||
|
|
||||||
// --- Mappa e Entità ---
|
// --- Mappa e Entita ---
|
||||||
// The generated maze: 0 = Wall, 1 = Floor, 2 = Victory Tile
|
|
||||||
extern uint8_t maze[MAP_SIZE][MAP_SIZE];
|
|
||||||
extern uint8_t player_lx;
|
extern uint8_t player_lx;
|
||||||
extern uint8_t player_ly;
|
extern uint8_t player_ly;
|
||||||
extern uint8_t player_dir; // 0=DR, 1=DL, 2=UL, 3=UR
|
extern uint8_t player_dir; // 0=DR, 1=DL, 2=UL, 3=UR
|
||||||
@@ -74,4 +74,4 @@ extern int16_t enemy_start_px, enemy_start_py;
|
|||||||
extern int16_t enemy_target_px, enemy_target_py;
|
extern int16_t enemy_target_px, enemy_target_py;
|
||||||
extern uint8_t enemy_cooldown;
|
extern uint8_t enemy_cooldown;
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
+60
-58
@@ -3,49 +3,56 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <rand.h>
|
#include <rand.h>
|
||||||
|
|
||||||
|
// Capacita' massima degli array di appoggio (in WRAM, statici per non saturare
|
||||||
|
// lo stack hardware del LR35902). Le stanze del DFS sono le celle dispari:
|
||||||
|
// al massimo (MAX_MAP_SIZE/2)^2 = 8*8 = 64 per un labirinto 17x17.
|
||||||
|
#define MAX_ROOMS ((MAX_MAP_SIZE / 2) * (MAX_MAP_SIZE / 2))
|
||||||
|
#define MAX_CELLS (MAX_MAP_SIZE * MAX_MAP_SIZE)
|
||||||
|
|
||||||
|
// Array di appoggio statici (in WRAM, non sullo stack hardware).
|
||||||
|
static uint8_t stack_x[MAX_ROOMS];
|
||||||
|
static uint8_t stack_y[MAX_ROOMS];
|
||||||
|
static uint8_t valid_x[MAX_CELLS];
|
||||||
|
static uint8_t valid_y[MAX_CELLS];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a randomized maze using a Depth-First Search (DFS) algorithm with backtracking.
|
* Generates a randomized maze using a Depth-First Search (DFS) algorithm with backtracking.
|
||||||
*
|
*
|
||||||
* Sviluppo & Scelte Architetturali:
|
* Sviluppo & Scelte Architetturali:
|
||||||
* 1. Perché DFS? Questo algoritmo è perfetto per labirinti garantendo che ogni cella sia
|
* 1. Perché DFS? Garantisce che ogni cella sia raggiungibile dalla partenza (nessuna isola).
|
||||||
* raggiungibile partendo dall'inizio (nessuna isola irraggiungibile).
|
* 2. Il labirinto è una griglia map_size x map_size (cresce col livello, da MAP_SIZE a
|
||||||
* 2. Il labirinto è una griglia MAP_SIZE x MAP_SIZE in cui le celle dispari (es. 1,1 o 3,3)
|
* MAX_MAP_SIZE) in cui le celle dispari (1,1 o 3,3) sono le stanze e le pari sono i muri
|
||||||
* sono le stanze, mentre le celle pari sono i muri divisori. Il DFS scava i muri saltando
|
* divisori. Il DFS scava i muri saltando di 2 in 2 e abbattendo il muro in mezzo.
|
||||||
* di 2 in 2 e abbattendo il muro in mezzo.
|
* 3. Fase post-DFS: riapre casualmente il 15% dei muri per creare loop (vitali per
|
||||||
* 3. Abbiamo aggiunto una fase post-DFS che rimuove casualmente qualche muro rimasto (con
|
* aggirare l'inseguitore).
|
||||||
* il 15% di probabilità). Questo spezza la struttura "perfetta" del labirinto creando
|
* 4. La BOTOLA (traguardo) viene piazzata su una cella a sufficiente distanza (Chebyshev
|
||||||
* dei percorsi ciclici (loop). I loop sono vitali in un gioco con un nemico che ti insegue,
|
* >= map_size/2) dalla partenza (1,1), in modo che sia sempre lontana ma su qualunque tile.
|
||||||
* permettendo al giocatore di aggirare il nemico scappando in cerchio.
|
|
||||||
* 4. La casella della VITTORIA viene piazzata alla fine. Usiamo la "Distanza di Manhattan"
|
|
||||||
* (somma della distanza X e Y) dalla partenza (1,1) per assicurarci che il traguardo
|
|
||||||
* sia la casella più lontana in assoluto.
|
|
||||||
*/
|
*/
|
||||||
void generate_maze(void) {
|
void generate_maze(void) {
|
||||||
// 1. Inizializziamo l'intera mappa a 0 (che rappresenta un Muro / Spazio Vuoto)
|
uint8_t sz = map_size;
|
||||||
|
|
||||||
|
// 1. Inizializziamo l'intera mappa a 0 (Muro / Spazio Vuoto)
|
||||||
memset(maze, 0, sizeof(maze));
|
memset(maze, 0, sizeof(maze));
|
||||||
|
|
||||||
// Stack array per il backtracking. La griglia 7x7 ha al massimo 49 celle dispari (stanze).
|
|
||||||
uint8_t stack_x[49];
|
|
||||||
uint8_t stack_y[49];
|
|
||||||
uint8_t stack_ptr = 0;
|
uint8_t stack_ptr = 0;
|
||||||
|
|
||||||
// Partiamo sempre dalle coordinate (1, 1) in alto a sinistra
|
// Partiamo sempre dalle coordinate (1, 1) in alto a sinistra
|
||||||
uint8_t cx = 1;
|
uint8_t cx = 1;
|
||||||
uint8_t cy = 1;
|
uint8_t cy = 1;
|
||||||
maze[cy][cx] = 1; // 1 = Pavimento calpestabile
|
maze[cy][cx] = 1; // 1 = Pavimento calpestabile
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
// Cerchiamo i vicini non visitati a distanza 2 (saltando il muro divisorio)
|
// Cerchiamo i vicini non visitati a distanza 2 (saltando il muro divisorio)
|
||||||
uint8_t nx[4];
|
uint8_t nx[4];
|
||||||
uint8_t ny[4];
|
uint8_t ny[4];
|
||||||
uint8_t count = 0;
|
uint8_t count = 0;
|
||||||
|
|
||||||
// Su
|
// Su
|
||||||
if (cy >= 3 && maze[cy - 2][cx] == 0) {
|
if (cy >= 3 && maze[cy - 2][cx] == 0) {
|
||||||
nx[count] = cx; ny[count] = cy - 2; count++;
|
nx[count] = cx; ny[count] = cy - 2; count++;
|
||||||
}
|
}
|
||||||
// Giù
|
// Giù
|
||||||
if (cy <= MAP_SIZE - 4 && maze[cy + 2][cx] == 0) {
|
if (cy <= sz - 4 && maze[cy + 2][cx] == 0) {
|
||||||
nx[count] = cx; ny[count] = cy + 2; count++;
|
nx[count] = cx; ny[count] = cy + 2; count++;
|
||||||
}
|
}
|
||||||
// Sinistra
|
// Sinistra
|
||||||
@@ -53,49 +60,48 @@ void generate_maze(void) {
|
|||||||
nx[count] = cx - 2; ny[count] = cy; count++;
|
nx[count] = cx - 2; ny[count] = cy; count++;
|
||||||
}
|
}
|
||||||
// Destra
|
// Destra
|
||||||
if (cx <= MAP_SIZE - 4 && maze[cy][cx + 2] == 0) {
|
if (cx <= sz - 4 && maze[cy][cx + 2] == 0) {
|
||||||
nx[count] = cx + 2; ny[count] = cy; count++;
|
nx[count] = cx + 2; ny[count] = cy; count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Se ci sono vicini validi
|
// Se ci sono vicini validi
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
// Scegli una direzione a caso tra quelle disponibili
|
// Scegli una direzione a caso tra quelle disponibili
|
||||||
uint8_t dir = rand() % count;
|
uint8_t dir = rand() % count;
|
||||||
|
|
||||||
// Salviamo la cella corrente nello stack per poterci tornare
|
// Salviamo la cella corrente nello stack per poterci tornare
|
||||||
stack_x[stack_ptr] = cx;
|
stack_x[stack_ptr] = cx;
|
||||||
stack_y[stack_ptr] = cy;
|
stack_y[stack_ptr] = cy;
|
||||||
stack_ptr++;
|
stack_ptr++;
|
||||||
|
|
||||||
// Abbattiamo il muro nel mezzo (calcolato facendo la media aritmetica delle coordinate)
|
// Abbattiamo il muro nel mezzo (media aritmetica delle coordinate)
|
||||||
maze[(cy + ny[dir]) / 2][(cx + nx[dir]) / 2] = 1;
|
maze[(cy + ny[dir]) / 2][(cx + nx[dir]) / 2] = 1;
|
||||||
|
|
||||||
// Spostiamo la posizione corrente sul vicino scelto
|
// Spostiamo la posizione corrente sul vicino scelto
|
||||||
cx = nx[dir];
|
cx = nx[dir];
|
||||||
cy = ny[dir];
|
cy = ny[dir];
|
||||||
maze[cy][cx] = 1;
|
maze[cy][cx] = 1;
|
||||||
}
|
}
|
||||||
// Se non ci sono vicini, torna indietro prendendo l'ultima cella salvata dallo stack
|
// Se non ci sono vicini, torna indietro prendendo l'ultima cella dallo stack
|
||||||
else if (stack_ptr > 0) {
|
else if (stack_ptr > 0) {
|
||||||
stack_ptr--;
|
stack_ptr--;
|
||||||
cx = stack_x[stack_ptr];
|
cx = stack_x[stack_ptr];
|
||||||
cy = stack_y[stack_ptr];
|
cy = stack_y[stack_ptr];
|
||||||
}
|
}
|
||||||
// Se lo stack è vuoto, abbiamo visitato tutte le celle: il labirinto è completo!
|
// Se lo stack è vuoto, abbiamo visitato tutte le celle: il labirinto è completo!
|
||||||
else {
|
else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FASE 2: Riapriamo casualmente dei muri per creare percorsi alternativi e loop (15% chance).
|
// FASE 2: Riapriamo casualmente dei muri per creare percorsi alternativi e loop (15%).
|
||||||
// Questo è cruciale per la giocabilità: permette al giocatore di aggirare l'inseguitore!
|
// Cruciale per la giocabilità: permette al giocatore di aggirare l'inseguitore!
|
||||||
for (uint8_t y = 1; y < MAP_SIZE - 1; y++) {
|
for (uint8_t y = 1; y < sz - 1; y++) {
|
||||||
for (uint8_t x = 1; x < MAP_SIZE - 1; x++) {
|
for (uint8_t x = 1; x < sz - 1; x++) {
|
||||||
if (maze[y][x] == 0) {
|
if (maze[y][x] == 0) {
|
||||||
// Se la casella è un muro che connette orizzontalmente o verticalmente due corridoi
|
// Muro che connette orizzontalmente o verticalmente due corridoi
|
||||||
if ((maze[y][x - 1] == 1 && maze[y][x + 1] == 1) ||
|
if ((maze[y][x - 1] == 1 && maze[y][x + 1] == 1) ||
|
||||||
(maze[y - 1][x] == 1 && maze[y + 1][x] == 1)) {
|
(maze[y - 1][x] == 1 && maze[y + 1][x] == 1)) {
|
||||||
// Genera numero casuale tra 0 e 99. Se < 15, trasforma in pavimento.
|
|
||||||
if ((rand() % 100) < 15) {
|
if ((rand() % 100) < 15) {
|
||||||
maze[y][x] = 1;
|
maze[y][x] = 1;
|
||||||
}
|
}
|
||||||
@@ -103,27 +109,24 @@ void generate_maze(void) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FASE 3: Posizionamento della Botola (Traguardo).
|
// FASE 3: Posizionamento della Botola (Traguardo).
|
||||||
// La botola viene piazzata su una cella calpestabile a "sufficiente distanza"
|
// La botola viene piazzata su una cella calpestabile a "sufficiente distanza" (Chebyshev
|
||||||
// dalla casella di partenza del giocatore (1,1), cosi' che il traguardo sia
|
// >= map_size/2) dalla partenza (1,1): sempre lontana, ma su una qualunque tile.
|
||||||
// sempre lontano ma possa trovarsi su una qualunque tile del labirinto,
|
// La soglia scala con la dimensione del labirinto (3 per 7x7, 8 per 17x17).
|
||||||
// non piu' vincolato al bordo sud. Usiamo la distanza di Chebyshev
|
uint8_t min_goal = sz / 2;
|
||||||
// (consistente con il resto dell'engine: fog of war, attivazione nemico).
|
if (min_goal < 3) min_goal = 3;
|
||||||
#define MIN_GOAL_DIST 3
|
|
||||||
uint8_t valid_x[MAP_SIZE * MAP_SIZE];
|
|
||||||
uint8_t valid_y[MAP_SIZE * MAP_SIZE];
|
|
||||||
uint8_t num_valid = 0;
|
uint8_t num_valid = 0;
|
||||||
|
|
||||||
for (uint8_t y = 1; y < MAP_SIZE - 1; y++) {
|
for (uint8_t y = 1; y < sz - 1; y++) {
|
||||||
for (uint8_t x = 1; x < MAP_SIZE - 1; x++) {
|
for (uint8_t x = 1; x < sz - 1; x++) {
|
||||||
if (maze[y][x] == 1) {
|
if (maze[y][x] == 1) {
|
||||||
int8_t dx = (int8_t)x - 1;
|
int8_t dx = (int8_t)x - 1;
|
||||||
int8_t dy = (int8_t)y - 1;
|
int8_t dy = (int8_t)y - 1;
|
||||||
if (dx < 0) dx = -dx;
|
if (dx < 0) dx = -dx;
|
||||||
if (dy < 0) dy = -dy;
|
if (dy < 0) dy = -dy;
|
||||||
int8_t dist = (dx > dy) ? dx : dy;
|
int8_t dist = (dx > dy) ? dx : dy;
|
||||||
if (dist >= MIN_GOAL_DIST) {
|
if (dist >= (int8_t)min_goal) {
|
||||||
valid_x[num_valid] = x;
|
valid_x[num_valid] = x;
|
||||||
valid_y[num_valid] = y;
|
valid_y[num_valid] = y;
|
||||||
num_valid++;
|
num_valid++;
|
||||||
@@ -140,14 +143,13 @@ void generate_maze(void) {
|
|||||||
stairs_lx = target_x;
|
stairs_lx = target_x;
|
||||||
stairs_ly = target_y;
|
stairs_ly = target_y;
|
||||||
} else {
|
} else {
|
||||||
// Fallback estremo: nessuna cella a sufficienza distante (teoricamente
|
// Fallback estremo: nessuna cella a sufficienza distante. Sceglie la cella
|
||||||
// impossibile in un perfect maze 7x7 con partenza 1,1, ma difensivo).
|
// calpestabile piu' lontana in assoluto da (1,1).
|
||||||
// Sceglie la cella calpestabile piu' lontana in assoluto da (1,1).
|
uint8_t best_x = sz - 2;
|
||||||
uint8_t best_x = MAP_SIZE - 2;
|
uint8_t best_y = sz - 2;
|
||||||
uint8_t best_y = MAP_SIZE - 2;
|
|
||||||
int8_t best_dist = -1;
|
int8_t best_dist = -1;
|
||||||
for (uint8_t y = 1; y < MAP_SIZE - 1; y++) {
|
for (uint8_t y = 1; y < sz - 1; y++) {
|
||||||
for (uint8_t x = 1; x < MAP_SIZE - 1; x++) {
|
for (uint8_t x = 1; x < sz - 1; x++) {
|
||||||
if (maze[y][x] == 1) {
|
if (maze[y][x] == 1) {
|
||||||
int8_t dx = (int8_t)x - 1;
|
int8_t dx = (int8_t)x - 1;
|
||||||
int8_t dy = (int8_t)y - 1;
|
int8_t dy = (int8_t)y - 1;
|
||||||
@@ -166,4 +168,4 @@ void generate_maze(void) {
|
|||||||
stairs_lx = best_x;
|
stairs_lx = best_x;
|
||||||
stairs_ly = best_y;
|
stairs_ly = best_y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2
-2
@@ -144,7 +144,7 @@ void update_player_movement(uint8_t keys, uint8_t prev_keys) {
|
|||||||
int8_t land_ly = player_ly + move_ly * 2;
|
int8_t land_ly = player_ly + move_ly * 2;
|
||||||
|
|
||||||
// Verifica che il salto cada all'interno del labirinto
|
// Verifica che il salto cada all'interno del labirinto
|
||||||
if (land_lx >= 0 && land_lx < MAP_SIZE && land_ly >= 0 && land_ly < MAP_SIZE) {
|
if (land_lx >= 0 && (uint8_t)land_lx < map_size && land_ly >= 0 && (uint8_t)land_ly < map_size) {
|
||||||
// Condizioni del salto:
|
// Condizioni del salto:
|
||||||
// a) La cella intermedia DEVE essere un muro (maze == 0)
|
// a) La cella intermedia DEVE essere un muro (maze == 0)
|
||||||
// b) La cella di arrivo DEVE essere un pavimento o vittoria (maze == 1 o 2)
|
// b) La cella di arrivo DEVE essere un pavimento o vittoria (maze == 1 o 2)
|
||||||
@@ -185,7 +185,7 @@ void update_player_movement(uint8_t keys, uint8_t prev_keys) {
|
|||||||
int8_t new_lx = player_lx + move_lx;
|
int8_t new_lx = player_lx + move_lx;
|
||||||
int8_t new_ly = player_ly + move_ly;
|
int8_t new_ly = player_ly + move_ly;
|
||||||
|
|
||||||
if (new_lx >= 0 && new_lx < MAP_SIZE && new_ly >= 0 && new_ly < MAP_SIZE) {
|
if (new_lx >= 0 && (uint8_t)new_lx < map_size && new_ly >= 0 && (uint8_t)new_ly < map_size) {
|
||||||
// Se la destinazione è pavimento normale o casella di vittoria
|
// Se la destinazione è pavimento normale o casella di vittoria
|
||||||
if (maze[new_ly][new_lx] == 1 || maze[new_ly][new_lx] == 2) {
|
if (maze[new_ly][new_lx] == 1 || maze[new_ly][new_lx] == 2) {
|
||||||
is_moving = 1;
|
is_moving = 1;
|
||||||
|
|||||||
+10
-7
@@ -11,7 +11,7 @@
|
|||||||
#include "maze.h"
|
#include "maze.h"
|
||||||
|
|
||||||
uint8_t get_tile_state(int8_t cx, int8_t cy, int8_t lx, int8_t ly) {
|
uint8_t get_tile_state(int8_t cx, int8_t cy, int8_t lx, int8_t ly) {
|
||||||
if (lx < 0 || lx >= MAP_SIZE || ly < 0 || ly >= MAP_SIZE) return 0;
|
if (lx < 0 || (uint8_t)lx >= map_size || ly < 0 || (uint8_t)ly >= map_size) return 0;
|
||||||
if (maze[ly][lx] == 0) return 0;
|
if (maze[ly][lx] == 0) return 0;
|
||||||
int8_t dx = lx - cx;
|
int8_t dx = lx - cx;
|
||||||
int8_t dy = ly - cy;
|
int8_t dy = ly - cy;
|
||||||
@@ -150,12 +150,12 @@ void draw_map(uint8_t center_x, uint8_t center_y) {
|
|||||||
int8_t start_x = center_x - 2;
|
int8_t start_x = center_x - 2;
|
||||||
if (start_x < 0) start_x = 0;
|
if (start_x < 0) start_x = 0;
|
||||||
int8_t end_x = center_x + 2;
|
int8_t end_x = center_x + 2;
|
||||||
if (end_x >= MAP_SIZE) end_x = MAP_SIZE - 1;
|
if ((uint8_t)end_x >= map_size) end_x = map_size - 1;
|
||||||
|
|
||||||
int8_t start_y = center_y - 2;
|
int8_t start_y = center_y - 2;
|
||||||
if (start_y < 0) start_y = 0;
|
if (start_y < 0) start_y = 0;
|
||||||
int8_t end_y = center_y + 2;
|
int8_t end_y = center_y + 2;
|
||||||
if (end_y >= MAP_SIZE) end_y = MAP_SIZE - 1;
|
if ((uint8_t)end_y >= map_size) end_y = map_size - 1;
|
||||||
|
|
||||||
// Processiamo la mappa in DUE passate per risolvere il problema dell'overlapping isometrico.
|
// Processiamo la mappa in DUE passate per risolvere il problema dell'overlapping isometrico.
|
||||||
// L'engine disegna da Nord a Sud. I tile a Sud sovrascrivono la metà inferiore dei tile a Nord.
|
// L'engine disegna da Nord a Sud. I tile a Sud sovrascrivono la metà inferiore dei tile a Nord.
|
||||||
@@ -248,10 +248,13 @@ void draw_map(uint8_t center_x, uint8_t center_y) {
|
|||||||
|
|
||||||
update_stamina_display();
|
update_stamina_display();
|
||||||
|
|
||||||
// Ottimizzazione hardware critica: trasferire tutta la mappa (1024 bytes) via set_bkg_tiles
|
// Trasferiamo l'intera mappa 32x32 (1024 byte) al Background hardware.
|
||||||
// causa lag e sfarfallii (VRAM access). Trasferiamo solo le righe da 2 a 17 (16 righe totali),
|
// Con labirinti grandi (map_size > 7) la finestra fog-of-war 5x5, proiettata in
|
||||||
// che è la porzione visibile del Game Boy (160x144 px) in cui si svolge l'azione.
|
// coordinate isometriche assolute, puo' cadere in righe OLTRE il vecchio range
|
||||||
set_bkg_tiles(0, 2, 32, 16, &map_buffer[2 * 32]);
|
// 2-17 (a causa del wrapping & 31), quindi non basta piu' flussare solo 16 righe.
|
||||||
|
// draw_map e' chiamato solo ai passi del movimento (non ogni frame), quindi il
|
||||||
|
// costo del flush completo e' sostenibile.
|
||||||
|
set_bkg_tiles(0, 0, 32, 32, map_buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user