Generazione Labirinti in C++

Ciao,

in questi giorni mi sto riprendendo dal COVID, quindi niente video per un po’. Detto questo avevo un po’ di codice C++ in giro sviluppato settimane fa e quindi volevo postarlo qui.

Il contesto di riferimento è la generazione di labirinti. Ho scritto un piccolo programma in C++ con le librerie SFML + ImgGui per ottenere il seguente risultato

Nel menu è possibile scegliere il tipo di algoritmo da utilizzare per la generazione del labirinto. Per adesso le opzioni sono due:

  • stack based
  • random uniform

La stack based è la più interessante in quanto utilizza l’algoritmo di vista dei grafi noto con il nome di Depth First Search (DFS) per ottenere il labirinto finale. Dato che durante la generazione vengono effettuate delle scelte pseudo-randomiche, il risultato finale cambia ad ogni generazione.

È possibile quindi generare il labirinto in modo istantaneo, oppure di vedere una piccola animazione che visualizza il processo di generazione frame by frame. Segue una piccola gif che mostra quando descritto.

2024-08-08 22-28-23

È poi possibile “risolvere” il labirinto, il che consiste nel trovare il cammino che collega la sorgente (il punto iniziale) ad una destinazione, che per adesso è hard-codata ad essere il punto più in basso a destra del labirinto. Anche la risoluzione può essere fatta in due modi diversi: se si clicca sulla flag live si può vedere una renderizzazione frame by frame del calcolo del percorso.

L’idea di base di questo tipo di generatori è molto semplice: si rappresenta il labirinto come un grafo, e abbiamo un muro tra due celle adiacenti solo quando queste non sono connesse da nessun arco.

Tramite una visita DFS siamo in grado di visitare le varie celle e durante questo processo di visita è possibile selezionare i muri da costruire.

La visita DFS fa utilizzo di una particolare strutture dati detta stack. Questa è una struttura dati di tipo LIFO (Last-In-First-Out), il che significa che gli elementi vengono tirati fuori nell’ordine inverso da quello di inserzione. Se prima aggiungo X e poi aggiungo Y, allora dopo l’elemento che estrarrò sarà Y, e poi X. Questo comportamento è simmetricalmente opposto a quello di una queue, un’altra struttura dati fondamentale che però lavora in modo FIFO (First-In-First-Out).

Il codice della visita è piuttosto semplice, ed è la parte più interessante, per questo la riporto a seguire

void Maze::initMazeStack() {
  // check if this is the first frame we're being called
  //
  if (init && workingStack.empty()) {    
    // initialize board for maze generation
    for (int y = 0; y < height; y++) {
      for (int x = 0; x < width; x++) {
	setAllWalls(y, x);
	grid[y][x].state = CellState::NOT_REACHED;
      }
    }
    workingStack.push(getPlayerPos());
  }
  
  
  do {
    Cell *c = workingStack.top();
    c->state = CellState::VISITED;
      
    std::vector<Wall> directions = getAdjValidDirections(c->y, c->x, false);
    if (directions.size() == 0) {
      workingStack.pop();
      if (init && workingStack.empty()) {
	init = false;
	return;
      }
      continue;
    }

    Wall direction = randomDirection(directions);
    Cell *adj = getAdjCell(c->y, c->x, direction);
    unsetWall(c->y, c->x, direction);
    adj->state = CellState::REACHED;
    workingStack.push(adj);
  } while (!liveInit && !workingStack.empty());

}

Da notare come la flag liveInit determina se abbiamo una visualizzazione frame by frame, oppure una generazione immediata del labirinto.

E niente, questo è un contesto in cui semplici e utili concetti ripresi dall’informatica più teorica (in particolare la teoria dei grafi, l’utilizzo di strutture dati fondamentali quali stack e queue, e gli algoritmi di visita), sono messi in pratica per ottenere delle divertenti applicazioni.

Segue il codice.

maze-solver.zip (761.3 KB)

Per eseguirlo bisogna scaricare la libreria SFML. Il setup è simile a quello mostrato nel seguente video:

3 Likes

Davvero interessante … sarebbe bello esplorare a fonodo come funzionano questi algoritmi, che di base non sono neanche troppo difficili concettualmente

Molto interessante! Adesso mi viene voglia di provare a scrivere una versione inversa, generando una griglia di partenza e “rompendo” i muri tra due celle collegate. Devo solo imparare il C++ … :smiling_face_with_tear:

Guarda il mio C++ è veramente basic e pessimo, lo sto esplorando giusto per divertimento ed in particolare perché sono abbastanza affascinato dalle potenzialità di librerie tipo ImGui per la creazione di GUI flessibili per strumentistica specializzata.

Si, penso anche io che per la costruzione di UI C++ sia probabilmente il linguaggio più solido in realtà. Anche perchè non penso ci siano molti altri linguaggi che riescano a portare buone librerie grafiche sul “basso livello”, senza dipendenze aggiuntive. Ma non sono decisamente un esperto, magari sbaglio.

Io ho sempre fatto una fatica pazzesca a trovare un buon linguaggio per le UI, alla fine mi riduco sempre a fare web app per ogni cosa.

C’è sempre il caro vecchio Swing altrimenti :joy:

Io negli ultimi giorni ho ripreso a giochicchiare con raylib, ho visto che c’è un nuovo modulo per creare delle gui (che dovrebbero essere abbastanza portabili) raygui.
È da capire se sono utlizzabili

Si raylib pure è una figata. Specialmente seguendo gli stream di Tsoding, che lo utilizza ad ogni stream quasi. La cosa bella di raylib è l’approccio usato: quello di essere il più semplice possibile, pur facendo cose interessanti e complesse.

Per quanto riguarda raygui dovrò approfondire, magari ne esce un video. Diciamo che volevo fare un video generale su raylib per far conoscere di più questa libreria super figa, anche per supportare lo sviluppatore, che ho sentito non riesce a guadagnare molto da queste cose, e quindi lo può fare solo come hobby secondario.

Sisi raylib è una figata assoluta, con la release della versione 5 sono state aggiunte un sacco di funzionalità nuove, un paio di “framework” tra cui raygui ed raymath.
Non sono framework nel senso che impongono una struttura precisa al codice o roba simile, sono più un plumbing di primitive di raylibg per semplificare alcuni use case.