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