Programmare in python: 2. Labirinto

Programmare in python: 2. Labirinto

Introduzione

    Nella Grecia antica, il labirinto non era soltanto un intreccio di muri, ma una prova di conoscenza. Secondo il mito, Dedalo lo costruì a Creta per imprigionare il Minotauro, ma fu Teseo, guidato dal filo di Arianna, a trasformarlo in un percorso di liberazione. Il labirinto è così divenuto un simbolo della mente umana, della ricerca e della logica: un luogo dove ci si perde per ritrovarsi, dove l’ordine nasce dal disordine.

Oggi, in informatica e matematica, il labirinto è un problema da generare e da risolvere. Ma dietro l’algoritmo — anche il più preciso — rimane sempre l’eco di quel gesto originario: cercare una via.

Un labirinto non è solo un insieme di muri e passaggi: è un linguaggio, una struttura logica che si costruisce passo dopo passo.

Generarlo significa tradurre un’idea astratta — la ricerca in profondità — in un percorso concreto.

In questo progetto, la generazione del labirinto nasce non da una formula predefinita, ma da una logica costruita “dal basso”: un’esplorazione iterativa, autonoma, quasi artigianale, che si sviluppa cella dopo cella come il pensiero che cerca il suo filo.

Risultato

    L’algoritmo di generazione del labirinto è stato realizzato in forma iterativa, con backtracking esplicito, al fine di rappresentare in modo trasparente la dinamica di esplorazione. È stato sviluppato in modo indipendente come costruzione logica autonoma e non deriva da implementazioni preesistenti.

A ogni passo, la cella corrente seleziona una direzione disponibile, apre il muro condiviso e procede verso la cella successiva, segnando così la traccia del percorso. Quando non esistono direzioni accessibili, l’algoritmo effettua un backtracking esplicito: il processo torna alla cella precedente, riprendendo l’esplorazione finché tutte le celle risultano visitate.

Questa scelta privilegia un approccio computazionale e costruttivo, più adatto a visualizzare la genesi del labirinto poiché rende visibile la logica di connessione interna del labirinto e la costruzione progressiva del suo spazio. La natura iterativa, oltre a garantire efficienza e controllo esplicito sul flusso di esecuzione, consente un’analisi diretta del comportamento dell’algoritmo e delle sue simmetrie spaziali.
Inoltre, eliminando i limiti di profondità tipici della ricorsione, evita dunque i problemi legati ai limiti dello stack e permette una gestione diretta dello stato e un controllo più immediato dell’esplorazione.
 


Link utili

▶ Esegui il programma su Replit:

  1. 1. Clicca sul linkApri su Replitper accedere al progetto.
  2. 2. Se non hai un account Replit, registrati gratuitamente oppure accedi con Google/GitHub.
  3. 3. Una volta nel progetto, premi “Run” per avviare l’esecuzione.
  4. 4. Puoi consultare il file sorgente e, se desideri, scaricarlo o copiarlo dal pannello di codice a sinistra.


▶ Scarica la versione portatile del codice:

  1. 1. Clicca sul link Scarica File per scaricare il file Labirinto.txt, che contiene il codice sorgente completo in formato .txt.
  2. 2. Rinomina il file cambiando l’estensione da .txt a .py.
  3. 3. Ora puoi eseguirlo localmente su qualsiasi IDE per python (es. VS Code, IntelliJ IDEA, Eclipse con PyDev o NetBeans con plugin Python) oppure direttamente da terminale con il comando: python3 Labirinto.py (oppure python Labirinto.py su Windows, se Python 3 è impostato come interprete predefinito), eseguendo il file con l’interprete Python.


▶ Utilizzo:


  1. 1. Requisiti
  • Il programma richiede il modulo Matplotlib per la visualizzazione grafica. Se non è installata nel proprio ambiente, è possibile aggiungerla tramite terminale con il comando:
 pip install matplotlib

  1. 2. Configurazione del reticolo
  • Le dimensioni del labirinto sono definite dai parametri:
XCOLONNE = 50 
YRIGHE = XCOLONNE
  • È possibile modificare questi valori per ottenere labirinti di dimensioni diverse, anche rettangolari. Le dimensioni attuali sono ottimizzate per un’esecuzione bilanciata; valori eccessivi non sono stati testati e potrebbero richiedere risorse computazionali elevate.

  1. 3. Visualizzazione e salvataggio
  • Il programma genera un’immagine del labirinto in formato .png.
    Tale file generato viene salvato nella directory di esecuzione del programma, con nome predefinito “labirinto”.
  • Se si desidera solo la visualizzazione a schermo, è sufficiente:
    • commentare la riga che salva l’immagine (plt.savefig(…)), e
    • decommentare quella che mostra il grafico (plt.show()), o viceversa.
  • È inoltre possibile disattivare la stampa testuale del reticolo (usata per analisi e debug) che elenca le celle nella forma:
    [id, (x+yj), [E, N, O, S], …] commentando la riga:
print(_reticolo)


Codice

    L’algoritmo prende forma nel codice come in una mappa che si disegna da sé: ogni cella, ogni muro abbattuto, restituisce il ritmo di un percorso logico che diventa spazio. L’implementazione, pur seguendo un approccio rigoroso e leggibile, conserva la sua impronta concettuale — un equilibrio tra calcolo e rappresentazione, dove la logica del labirinto si rivela attraverso la propria espressione matematica.

Labirinto Python
Labirinto.py
1
2"""
3Labirinto
4
5
6Descrizione:
7 Generazione di un labirinto “perfect maze” con visita in profondità (DFS) iterativa
8 e backtracking esplicito.
9 Le celle sono identificate da coordinate complesse (x + y*1j).
10 Ogni cella contiene: [id_visita, posizione, muri, direzioni_possibili]
11 dove l’ordine dei muri è: [Est, Nord, Ovest, Sud] (1 = muro presente, 0 = muro aperto),
12 e l’ordine delle direzioni è: [Est, Nord, Ovest, Sud]
13 (1 = direzione ammessa, 0 = direzione non ammessa).
14
15 Nota:
16 L’algoritmo presenta analogie con una visita in profondità (Depth-First Search, DFS)
17 nella sua versione iterativa, ma non deriva da implementazioni preesistenti.
18 È stato sviluppato in modo indipendente come costruzione logica autonoma,
19 basata sull'idea di avanzare finché esistono direzioni ammesse e tornare
20 alla cella precedente quando non ve ne sono, fino a completare la copertura del reticolo.
21
22Algoritmo (sintesi):
23 – Partenza da una cella sul perimetro (bordo aperto in ingresso) scelta in modo casuale
24 e setup del muro sul perimetro.
25 – Uscita: cella speculare rispetto all’ingresso e setup del muro sul perimetro.
26 – Scelta casuale di una direzione ammessa, apertura dei muri condivisi,
27 marcatura e avanzamento alla cella successiva.
28 – Se non ci sono direzioni disponibili: backtrack alla cella precedente.
29
30Parametri:
31 XCOLONNE, YRIGHE → dimensioni della griglia (1-based).
32
33Visualizzazione:
34 – Matplotlib: rendering dei muri con LineCollection.
35 – Stampa testuale: elenco delle celle come liste:
36 [id, (x+yj), [E, N, O, S], …] su stdout (per debug/analisi).
37
38
39
40Autore: Alessio Severi
41Licenza: MIT License
42
43MIT License
44
45Copyright (c) 2025 Alessio Severi
46
47Permission is hereby granted, free of charge, to any person obtaining a copy
48of this software and associated documentation files (the "Software"), to deal
49in the Software without restriction, including without limitation the rights
50to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
51copies of the Software, and to permit persons to whom the Software is
52furnished to do so, subject to the following conditions:
53
54The above copyright notice and this permission notice shall be included in all
55copies or substantial portions of the Software.
56
57THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
58IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
59FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
60AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
61LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
62OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
63SOFTWARE.
64
65"""
66
67
68
69import matplotlib.pyplot as plt
70from matplotlib.collections import LineCollection
71import random as rn
72
73
74
75# ======================
76# CONFIGURAZIONE
77# =====================
78
79
80# Dimensioni reticolo
81XCOLONNE = 50
82YRIGHE = XCOLONNE
83
84
85
86# ======================
87# FUNZIONI PRINCIPALI
88# ======================
89
90def setupReticolo() -> tuple[list, list, list, list, list, list, list]:
91
92 # per ogni cella: [id_visita, posizione_cella, muri := [est := 1, nord := 1, ovest:= 1, sud := 1],
93 # direzioni_possibili := [est := 1, nord := 1, ovest:= 1, sud := 1]]
94 reticolo_interno = [[None, complex(x, y), [1, 1, 1, 1], [1, 1, 1, 1]] for x in range(2,XCOLONNE) for y in range(2,YRIGHE) ]
95
96 lato_up = [[None, complex(x, YRIGHE), [1, 1, 1, 1], [1, 0, 1, 1]] for x in range(2, XCOLONNE)]
97 lato_down = [[None, complex(x, 1), [1, 1, 1, 1], [1, 1, 1, 0]] for x in range(2, XCOLONNE)]
98 lato_left = [[None, complex(1, y), [1, 1, 1, 1], [1, 1, 0, 1]] for y in range(2, YRIGHE)]
99 lato_right = [[None, complex(XCOLONNE, y), [1, 1, 1, 1], [0, 1, 1, 1]] for y in range(2, YRIGHE)]
100 top_left= [None, complex(1, YRIGHE), [1, 1, 1, 1], [1, 0, 0, 1]]
101 top_right= [None, complex(XCOLONNE, YRIGHE), [1, 1, 1, 1], [0, 0, 1, 1]]
102 bottom_left= [None, complex(1, 1), [1, 1, 1, 1], [1, 1, 0, 0]]
103 bottom_right= [None, complex(XCOLONNE, 1), [1, 1, 1, 1], [0, 1, 1, 0]]
104
105 # settaggio iniziale e definizione del reticolo
106 reticolo= [*reticolo_interno, *(perimetro_reticolo := [*lato_up, *lato_down, *lato_right, *lato_left,
107 top_left, top_right, bottom_left, bottom_right])]
108
109 return reticolo, lato_right, lato_left, lato_down, bottom_left, bottom_right, perimetro_reticolo
110
111
112
113
114def indicizzatore(lab: list) -> callable:
115
116 # mappatura: posizione complessa -> cella
117 by_pos = {c[1]: c for c in lab}
118
119 # mappatura: id -> cella (verrà costruito durante il settaggio del labirinto
120 # nella funzione setMuri)
121 by_id = {}
122
123 def trova(per = None, valore = None):
124
125 # lookup O(1) per posizione (complex)
126 if per == "pos":
127 return by_pos.get(valore)
128
129 # lookup O(1) per id (dopo l’assegnazione)
130 elif per == "id":
131 return by_id.get(valore)
132
133 # registra/aggiorna mappatura id->cella
134 elif per == "aggiorna":
135 by_id[valore[0]] = valore
136
137 else:
138 return None
139
140 return trova
141
142
143
144def openInOut(cellaStart: list, lato_right: list, lato_left: list, lato_down: list,
145 bottom_left: list, bottom_right: list, trova: callable) -> complex:
146
147 # settaggio id_visita – enumerazione della cella start
148 cellaStart[0]= 1
149
150
151 # scelta della cella end: riflessione speculare rispetto la cella start
152 xyCellaEnd = complex(XCOLONNE + 1, YRIGHE + 1) – cellaStart[1]
153
154 # ingresso: bordo destro → apertura muro est della cella start
155 if cellaStart in lato_right:
156 cellaStart[2][0] = 0
157
158 # apertura muro ovest della cella end
159 muro_out = 2
160
161 # ingresso: bordo basso → apertura muro sud della cella start
162 elif cellaStart in [*lato_down, bottom_left, bottom_right]:
163 cellaStart[2][3] = 0
164
165 # apertura muro nord della cella end
166 muro_out = 1
167
168 # ingresso: bordo sinistro → apertura muro ovest della cella start
169 elif cellaStart in lato_left:
170 cellaStart[2][2] = 0
171
172 # apertura muro est della cella end
173 muro_out = 0
174
175 # ingresso: bordo top → apertura muro nord della cella start
176 else:
177 cellaStart[2][1] = 0
178
179 # apertura muro sud della cella end
180 muro_out = 3
181
182 # settaggio muro della cella end
183 c = trova(per = "pos", valore = xyCellaEnd)
184 c[2][muro_out] = 0
185
186 # opzionale, se si vuol sapere dove si trova l’uscita
187 return xyCellaEnd
188
189
190
191# settaggio muri del reticolo, escluso il perimetro
192def setMuri(cellaStart: list, trova: callable) -> list:
193
194
195 # temp id max della cella visitata quando si ritorna ad una cella già visitata
196 enum = 0
197
198 # lista costante di sistema
199 COL = list(range(1,5))
200
201
202 while(True):
203
204 # Caso base: quando tutte le celle del labirinto sono visitate tutti i muri sono stati settatati
205 if cellaStart[0] == YRIGHE * XCOLONNE:
206 return cellaStart
207
208
209 # se la somma dei valori associati alle direzioni ammesse
210 # della cella corrente è non nulla allora si può andare avanti con l'esplorazione
211 if sum(cellaStart[3]):
212
213 # calcolo implicitamente le direzioni possibili e quindi anche il muro candidato per essere abbattuto
214 interval = [(a * b) –1 for a, b in zip(cellaStart[3], COL) if a]
215
216 # scelgo una direzione sapendo che per costruzione è presente un muro candidato per essere abbattuto
217 var = rn.choice(interval)
218
219 # calcolo le coordinate nel piano di Gauss della cella adiacente che posso esplorare
220 xyCellaNext = cellaStart[1] + (complex(0,1) ** var)
221
222
223 # ricerca della cella successiva (adiacente a quella corrente) del reticolo sapendo
224 # le sue coordinate nel piano di Gauss
225 cellaNext = trova(per = "pos", valore = xyCellaNext)
226
227 # considero se la cella adiacente a quella corrente non è mai stata visitata
228 if not cellaNext[0]:
229
230 # l'id riparte dall'ultima cella esplorata (con id max)
231 if not enum:
232 cellaNext[0] = cellaStart[0] +1
233
234 # l'id prosegue dall'ultima cella esplorata
235 else:
236 cellaNext[0] = enum +1
237 enum = 0
238
239 # aggiorna la mappatura id→cella per lookup O(1)
240 trova(per = "aggiorna", valore = cellaNext)
241
242
243 # si effettua la xor fra i due valori in binario e il risultato lo esprimo in decimale
244 # per abbattere/settare il muro della cella adiacente
245 cellaNext[2][var ^ 2] = 0
246
247 # poiché ora il muro è aperto non è possibile esplorare verso quella direzione
248 cellaNext[3][var ^ 2] = 0
249
250 # setto/abbatto il muro della cella corrente
251 cellaStart[2][var] = 0
252
253 # poiché ora il muro è aperto non è possibile esplorare verso quella direzione
254 cellaStart[3][var] = 0
255
256 # se la cella adiacente a quella corrente è già stata visitata
257 # non è possibile andare in quella direzione
258 else:
259 cellaStart[3][var] = 0
260
261 # la cella successiva rimane quella corrente
262 # cellaNext = cellaStart
263 continue
264
265
266 # poiché non posso andare in avanti con l'esplorazione torno indietro alla cella precedente
267 else:
268
269 # ricerca della cella precedete nel reticolo
270 cellaNext = trova(per = "id", valore = cellaStart[0] – 1)
271
272 # salvo l'id massimo della cella corrente tra quelle esplorate
273 # in modo da ripartire da questo valore quando esploro il reticolo
274 if enum < cellaNext[0] + 1:
275 enum = cellaNext[0] + 1
276
277
278 cellaStart = cellaNext
279
280
281
282# rendering del reticolo
283def rendering(celle: list) -> list:
284
285 segments = []
286
287 # per ogni cella viene creata una lista dei segmenti,
288 # corrispondenti ai muri del labirinto:
289 # sapendo le coordinate di ogni cella, derivanti dal numero complesso associato,
290 # del vertice in alto a destra del quadrato (cella) e sapendo se un muro (lato su di esso)
291 # è presente o abbattuto grazie alla lista
292 # muri := [est := 1, nord := 1, ovest:= 1, sud := 1] ove
293 # 1 rappresenta muro presente e 0 muro non presente (abbattuto)
294 for cella in celle:
295 x, y = int(round(cella[1].real)), int(round(cella[1].imag))
296
297 # Nord
298 if cella[2][1]:
299 segments.append([(x-1, y), (x, y)])
300
301 # Est
302 if cella[2][0]:
303 segments.append([(x, y-1), (x, y)])
304
305 # Ovest: solo bordo sinistro (x = 1)
306 if x == 1 and cella[2][2]:
307 segments.append(((0, y-1), (0, y)))
308
309 # Sud: solo bordo basso (y = 1)
310 if y == 1 and cella[2][3]:
311 segments.append(((x-1, 0), (x, 0)))
312
313
314 # crea la figura (area di disegno interna destinata ai contenuti grafici, 8×8 pollici)
315 # e il piano cartesiano su cui vine disegnato il labirinto (AxesSubplot)
316 fig, ax = plt.subplots(figsize=(8, 8))
317
318 # settaggio titolo
319 fig.canvas.manager.set_window_title("Labirinto")
320
321 # crea la collezione di linee
322 lc = LineCollection(
323 segments,
324 linewidths = 2,
325 colors = 'black',
326 capstyle = 'projecting', # (square) estende la linea oltre l’endpoint
327 joinstyle = 'miter', # spigolo vivo agli angoli
328 antialiased = False # evita filini bianchi da smoothing
329 )
330
331 # aggiunge ad ax
332 ax.add_collection(lc)
333
334 # imposta proporzioni
335 ax.set_aspect("equal", adjustable="box")
336
337 # padding per evitare il clipping del bordo
338 eps = 0.5
339
340 # imposta limiti
341 ax.set_xlim(-eps, XCOLONNE + eps)
342 ax.set_ylim(-eps, YRIGHE + eps)
343
344 # assi non visibili
345 ax.axis('off')
346
347 # visualizzazione:
348 # in ambienti headless (come Replit) non è possibile aprire una finestra grafica;
349 # il grafico viene quindi salvato come immagine .png.
350 plt.savefig("labirinto.png", dpi=300, bbox_inches="tight")
351 print("✅ Labirinto salvato come 'labirinto.png' (usa plt.show() in locale per visualizzarlo).")
352
353 # in locale puoi decommentare la riga seguente per visualizzare la finestra interattiva:
354 # plt.show()
355
356 return celle
357
358
359
360
361# ======================
362# PUNTO DI INGRESSO
363# ======================
364
365if __name__ == "__main__":
366
367 # setup iniziale: reticolo_interno, lati, angoli, perimetro, reticolo finale
368 _reticolo, _lato_right, _lato_left, _lato_down, _bottom_left, _bottom_right, _perimetro_reticolo = setupReticolo()
369
370 # scelta della cella iniziale
371 _cellaStart = rn.choice(_perimetro_reticolo)
372
373 # ottimizzazione per la ricerca di una cella nel reticolo
374 func = indicizzatore(_reticolo)
375
376 # apertura del muro che insiste sul perimetro del labirinto della cella iniziale
377 # e della cella finale (speculare alla cella iniziale)
378 openInOut( _cellaStart, _lato_right, _lato_left, _lato_down, _bottom_left, _bottom_right, func)
379
380 # genera il labirinto
381 setMuri(_cellaStart, func)
382
383 # Stampa testuale: elenco delle celle come liste:
384 # [id, (x+yj), [E, N, O, S], …] su stdout (per debug/analisi).
385 print(_reticolo)
386
387 # disegno del labirinto
388 rendering(_reticolo)
389
390
391