enne2 ae0706ccfb Place the hatch on any sufficiently-distant tile, not just the south edge
The victory hatch (maze[y][x]==2) is now chosen among all walkable cells
at Chebyshev distance >= MIN_GOAL_DIST (3) from the player start (1,1),
instead of being constrained to the bottom row. This lets the goal appear
anywhere in the maze while always staying far from the start.

- maze.c: collect all floor cells with chebyshev(1,1) >= 3 and pick one
  at random; fallback to the farthest floor cell if none qualifies.
- doc/generation.md + report: document the new placement rule.
2026-06-24 11:18:31 +02:00

A Scream from the Dark

Un survival-horror procedurale in prospettiva isometrica per Game Boy (DMG/CGB), scritto in C con GBDK-2020. Sei imprigionato in un labirinto 7×7 generato casualmente, illuminato solo da un ristretto quadrato di visibilità. Un fantasma si nasconde nel buio e ti bracca non appena entri nel suo raggio visivo. L'unica via di fuga è la botola sul bordo sud della mappa: raggiungerla significa "sprofondare più giù" (Going Deeper) e generare un nuovo livello.


🎮 Caratteristiche

  • Proiezione isometrica 2.5D: rendering della mappa a diamante (tile 32×16 px) su schermo Game Boy, con autotiling dinamico.
  • Labirinto casuale: algoritmo DFS iterativo con stack in WRAM (per evitare overflow dello stack hardware) che genera un "perfect maze" 7×7 unico ad ogni partita, poi "rotto" con riaperture casuali al 15% per creare loop e percorsi alternativi.
  • Fog of War: visibilità 5×5 basata sulla distanza di Chebyshev, con affievolimento della luce sui bordi. Il fantasma si attiva solo quando entra in questo riquadro.
  • Movimento interpolato (LERP): spostamenti fluidi del personaggio e della telecamera su 16 tick a punto fisso (no float).
  • 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.
  • 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).
  • Schermate a tutto schermo: copertina 2-bit nativa per il titolo, immagine "Going Deeper" per la vittoria, metasprite "GAME OVER" per la sconfitta.
  • Test headless: pipeline di verifica con PyBoy + OpenCV senza emulatore grafico.

Soundtrack

  1. Title Theme: solenne e misteriosa, 32 battute sugli accordi La minore, Sol, Fa e Mi.
  2. Gameplay Theme: battito ritmico ansioso ("eerie pulse") che accelera la tensione dell'inseguimento.
  3. Game Over Theme: concerto tragico polifonico di 128 note con percussioni (noise channel), basso virtuoso e drammatica discesa melodica.
  4. Going Deeper: melodia misteriosa discendente di 96 step (Am → Fmaj7 → Dm → E7 → C aug → abisso).

🛠️ Dettagli tecnici

Architettura dei file

  • main.c: entry point, loop VBL sincronizzato, macchina a stati app_state (0 = title, 1 = game).
  • engine.c / engine.h: "direttore d'orchestra" — title_init/update, engine_init, engine_update.
  • globals.c / globals.h: stato globale centralizzato (mappa, camera, player, enemy, stamina, game_over) per evitare dipendenze circolari tra moduli.
  • maze.c: generazione procedurale DFS + loop + posizionamento botola.
  • player_logic.c: input, DAS, state machine del movimento, salto, stamina.
  • enemy_logic.c: AI greedy, cooldown, rendering nemico, hitbox pixel-perfect.
  • render.c: proiezione isometrica, fog of war, autotiling multi-pass, stamina UI, sprite player.
  • sound.c: sequencer audio via VBL interrupt, 4 tracce.
  • tiles.c / player.c / enemy.c / gameover.c / next_level.c / stamina.c / title_bg.c: asset generati da png2asset.
  • scripts/: generazione procedurale di tile/sprite (generate_assets.py, generate_enemy.py), quantizzazione immagini (process_next_level.py), test headless.

Formato delle coordinate isometriche

Le coordinate logiche (lx, ly) vengono convertite in coordinate schermo (iso_x, iso_y):

iso_x = (lx - ly) * 2 + 12
iso_y = (lx + ly) * 1 + 2

e in coordinate pixel fisiche per camera/collisione:

px = (lx - ly) * 16 + 96
py = (lx + ly) * 8  + 16

Camera centrata: scroll_x = px - 64, scroll_y = py - 72.

Per un'analisi approfondita di codice, funzionalità e workaround storici, vedi doc/AScreamFromTheDark_report.md.


🚀 Requisiti e build

Prerequisiti

  1. GBDK-2020 installato in /home/enne2/.local/gbdk.
  2. Python 3 con i pacchetti per la rigenerazione degli asset e i test:
    pip install --user Pillow pyboy opencv-python numpy
    

Compilazione

make clean && make

Questo comando:

  1. Esegue gli script Python per creare tiles.png, player.png, enemy.png.
  2. Usa png2asset per convertire i PNG in sorgenti C.
  3. Usa il compilatore lcc di GBDK per compilare e linkare i sorgenti in build/hello_iso.gb (e build/test_gameover.gb).

🧪 Test e analisi automatica

  1. Screenshotpython3 scripts/test_pyboy.py: avvia la ROM in PyBoy per 120 frame e salva assets/hello_iso_gb.png.
  2. Test di movimento in WRAMpython3 scripts/test_movement.py: legge la griglia del labirinto in WRAM (indirizzo risolto dinamicamente via hello_iso.noi) e simula pressioni direzionali verificando player_lx/ly.
  3. Rilevamento glitch con OpenCVpython3 scripts/opencv_analyze_tiles.py: esamina lo screenshot cercando disallineamenti o buchi neri tra le giunzioni isometriche.
  4. ROM di test isolatamake build/test_gameover.gb: renderizza solo player + metasprite GAME OVER per validare la schermata di sconfitta.

La documentazione tecnica dettagliata per modulo è in doc/.

S
Description
A Scream from the Dark — procedural isometric survival-horror for Game Boy (DMG/CGB)
Readme 2.6 MiB
2026-07-01 09:38:18 +00:00
Languages
C 75.3%
Python 23.3%
Makefile 1.4%