-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcircular_maze.py
250 lines (208 loc) · 10.9 KB
/
circular_maze.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
#!/usr/bin/env python3
import math, random
from PIL import Image, ImageDraw
def generate_maze(
num_levels = 16, #number of concentric circles in the maze
big_level_every = 4, #big levels will have fewer gaps and be drawn with a thicker line
img_size = 2000, #number of pixels for the image's width and height
max_arc_length = .8, #max arc length of a segment before it needs to split into 2
max_gaps_ratio = .25, #the max proportion of gaps compared to total segments
narrow_line_width = .002, #how thick to make a normal level's lines
wide_line_width = .007, #how thick to make a big level's lines
foreground_color = 'black', #color of the maze's lines
background_color = '#FFF0', #color of the image background
debug = False, #should it show the missing gridlines in light blue
):
img = Image.new('RGBA', (img_size, img_size), background_color)
draw = ImageDraw.Draw(img)
def draw_arc(draw, img_size, radius, start_degrees, end_degrees, line_width=narrow_line_width, color=foreground_color):
dist_from_edge = round(img_size/2 - radius)
start_degrees, end_degrees = 360-end_degrees, 360-start_degrees
draw.arc([(dist_from_edge, dist_from_edge), (img_size-dist_from_edge, img_size-dist_from_edge)], start=start_degrees, end=end_degrees, fill=color, width=round(line_width*img_size))
def polar_to_xy(img_size, radius, angle_degrees):
return img_size/2+radius*math.cos(-angle_degrees*math.pi/180), img_size/2+radius*math.sin(-angle_degrees*math.pi/180)
def draw_ray(draw, img_size, radius1, radius2, angle_degrees, line_width=narrow_line_width, color=foreground_color):
push_inward = .001*img_size #tweak the radii slightly inward to make them be centered in the arc
prev_x, prev_y = polar_to_xy(img_size, radius1-push_inward, angle_degrees)
x, y = polar_to_xy(img_size, radius2-push_inward, angle_degrees)
draw.line((prev_x,prev_y, x,y), fill=color, width=round(line_width*img_size))
def get_level_radius(img_size, num_levels, level_idx):
return round(img_size/2/num_levels*(.5+level_idx))
MAZE_RAYS = 0
MAZE_ARCS = 1
maze = []
prev_radius = 0
num_segments = 4
for level_idx in range(num_levels):
radius = get_level_radius(img_size, num_levels, level_idx)
#old method of automatically determining number of segments, works except they don't line up with the previous
# segment_degrees = img_size*2/radius
# if level_idx == 0:
# segment_degrees = img_size/radius
# num_segments = round(360/segment_degrees)
segment_length = 2*math.pi*radius/num_segments
# if level_idx % 4 == 0:
if segment_length > img_size*max_arc_length/num_levels:
num_segments *= 2
maze.append([[],[]])
for cell_idx in range(num_segments):
maze[level_idx][MAZE_RAYS].append(0)
maze[level_idx][MAZE_ARCS].append(1)
# maze = [
# # r,t,l,b
# [[0,0,0,0], #rays
# #tr,tl,bl,br
# [0,1,1,1]], #arcs
# [[1,1,1,1,1,1,1,1], #rays
# [1,1,1,1,1,1,1,1]], #arcs
# [[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], #rays
# [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]], #arcs
# [[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], #rays
# [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]], #arcs
# [[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], #rays
# [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]], #arcs
# [[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], #rays
# [0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]], #arcs
# [[0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], #rays
# [0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]], #arcs
# [[0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], #rays
# [0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]], #arcs
# ]
def get_neighbors(maze, node):
neighbors = []
if node == 'END': #special case, mostly node is a tuple
prev_level_idx = len(maze)-1
for cell_idx in range(len(maze[prev_level_idx][MAZE_ARCS])):
if not maze[prev_level_idx][MAZE_ARCS][cell_idx]:
neighbors.append((prev_level_idx, cell_idx))
return neighbors
level_idx, cell_idx = node
level_num_cells = len(maze[level_idx][MAZE_RAYS])
prev_level_idx = level_idx-1
next_level_idx = level_idx+1
prev_cell_idx = (cell_idx-1) % level_num_cells
next_cell_idx = (cell_idx+1) % level_num_cells
if not maze[level_idx][MAZE_RAYS][cell_idx]:
neighbors.append((level_idx, prev_cell_idx))
if not maze[level_idx][MAZE_RAYS][next_cell_idx]:
neighbors.append((level_idx, next_cell_idx))
if not maze[level_idx][MAZE_ARCS][cell_idx]:
if next_level_idx == len(maze):
neighbors.append('END')
else:
next_level_num_cells = len(maze[next_level_idx][MAZE_RAYS])
if next_level_num_cells == level_num_cells:
neighbors.append((next_level_idx, cell_idx))
elif next_level_num_cells == level_num_cells * 2:
neighbors.append((next_level_idx, cell_idx*2))
neighbors.append((next_level_idx, cell_idx*2+1))
else:
raise Exception(f'expected {level_num_cells * 2} cells on level {next_level_idx} but got {next_level_num_cells}')
if prev_level_idx >= 0:
prev_level_num_cells = len(maze[prev_level_idx][MAZE_RAYS])
if prev_level_num_cells == level_num_cells:
if not maze[prev_level_idx][MAZE_ARCS][cell_idx]:
neighbors.append((prev_level_idx, cell_idx))
elif prev_level_num_cells == int(level_num_cells / 2):
if not maze[prev_level_idx][MAZE_ARCS][int(cell_idx/2)]:
neighbors.append((prev_level_idx, int(cell_idx/2)))
else:
raise Exception(f'expected {int(level_num_cells / 2)} cells on level {next_level_idx} but got {next_level_num_cells}')
return neighbors
for level_idx, level in enumerate(maze):
rays = level[MAZE_RAYS]
arcs = level[MAZE_ARCS]
is_big_level = (level_idx+1)/big_level_every == int((level_idx+1)/big_level_every)
if is_big_level:
max_gaps = round(len(arcs)*max_gaps_ratio)
num_gaps = random.randint(1, max(1, max_gaps))
else:
max_gaps = round(len(arcs)*max_gaps_ratio)
num_gaps = random.randint(3, max(3, max_gaps))
# num_gaps = num_gaps * 2 - 1 #always odd number
for i in range(len(rays)):
rays[i] = 0 #start with no sub-divisions on the level
arcs[i] = 1 #start with no gaps on the level
if level_idx == 0: #level 0 always has 4 arcs
num_gaps = 1
num_gaps = random.randint(-1,2)
if num_gaps < 1:
num_gaps = 1
# if level_idx == len(maze)-1:
# num_gaps = 1
# print(max_gaps, num_gaps)
indexes = [i for i in range(len(arcs))]
while num_gaps > 0:
if len(indexes) == 0:
break #abort if there are no places left to put gaps
# raise Exception("not enough options for gaps left")
gap_idx = random.choice(indexes)
indexes.remove(gap_idx)
if is_big_level:
dist = max(2, round(len(arcs) / 5)) #make sure the gaps on big levels are further away
else:
dist = 1
for i in range(-dist, dist+1):
#make sure there are no adjacent gaps:
try:
indexes.remove((gap_idx-i) % len(arcs))
except:
pass
arcs[gap_idx] = 0
num_gaps -= 1
def validate_maze(maze):
connected_nodes = [(0,0)] #0,0 is the top-right corner of the center circle
#the 1st coord is the level, the 2nd coord is the cell, starting from the cell sitting on the right flat line
connected_node_idx = 0
while True:
curr_node = connected_nodes[connected_node_idx]
for node in get_neighbors(maze, curr_node):
if not node in connected_nodes:
connected_nodes.append(node)
connected_node_idx += 1
if connected_node_idx >= len(connected_nodes):
break
#make sure all nodes are reachable by comparing the total number of reachable nodes with the expected number
return len(connected_nodes) == sum(map(lambda l: len(l[MAZE_RAYS]), maze))+1 #length of maze plus 1 for the 'END' node
assert validate_maze(maze)
rays_possible_to_add = []
for level_idx in range(1, len(maze)):
for ray_idx in range(0, len(maze[level_idx][MAZE_RAYS])):
rays_possible_to_add.append((level_idx, ray_idx))
#try each ray once but in a random order
while len(rays_possible_to_add) > 0:
ray = random.choice(rays_possible_to_add)
rays_possible_to_add.remove(ray)
level_idx, ray_idx = ray
if not maze[level_idx][MAZE_RAYS][ray_idx]:
maze[level_idx][MAZE_RAYS][ray_idx] = 1
if not validate_maze(maze):
maze[level_idx][MAZE_RAYS][ray_idx] = 0
prev_radius = 0
num_segments = 4
for level_idx in range(num_levels):
radius = get_level_radius(img_size, num_levels, level_idx)
is_big_level = (level_idx+1)/big_level_every == int((level_idx+1)/big_level_every)
if is_big_level:
arc_line_width = wide_line_width
else:
arc_line_width = narrow_line_width
num_segments = len(maze[level_idx][MAZE_ARCS])
segment_degrees = 360/num_segments
for segment_idx in range(num_segments):
# print(level_idx, segment_idx, end=' ')
# print(maze[level_idx][MAZE_RAYS][segment_idx], maze[level_idx][MAZE_ARCS][segment_idx])
# if (segment_idx+level_idx+3)/5 != int((segment_idx+level_idx+3)/5):
if maze[level_idx][MAZE_RAYS][segment_idx]:
draw_ray(draw, img_size, prev_radius, radius, segment_degrees * segment_idx)
elif debug:
draw_ray(draw, img_size, prev_radius, radius, segment_degrees * segment_idx, color='lightblue') #tmp
# if (segment_idx+level_idx)/8 != int((segment_idx+level_idx)/8):
if maze[level_idx][MAZE_ARCS][segment_idx]:
draw_arc(draw, img_size, radius, segment_degrees*segment_idx, segment_degrees*(segment_idx+1), arc_line_width)
elif debug:
draw_arc(draw, img_size, radius, segment_degrees*segment_idx, segment_degrees*(segment_idx+1), arc_line_width, color='lightblue') #tmp
prev_radius = radius
# img.show()
# img.save('test.png')
return img