# Maze generator, version 1 # Copyright (C) 2024 Foxarc # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . extends Node var language = TranslationServer.get_loaded_locales().find(TranslationServer.get_locale().left(-3)) # For every case size, / 1 2 3 4 5 6 7 8 9 var max_cases = [0, [57,27], [28,13], [19,9], [14,6], [11,5], [9,4], [8,3], [7,3], [6,3]] var offset = [0, [0,0], [10,10], [0,0], [10,30], [20,20], [30,30], [10,60], [10,30], [30,0]] var render_cells = [] var time_spent = 0 var generating = false var JS_animation = [] func clear_cells(): for i in range(render_cells.size()): for j in range(render_cells[i].size()): render_cells[i][j].queue_free() render_cells.clear() func rerender_cells(new_x,new_y): if $"Export".visible: $"Export".visible = false clear_cells() $"Progress".visible = false var border = StyleBoxFlat.new() border.bg_color = Color("303030") border.border_color = Color("ffffff") border.border_width_top = 1 border.border_width_right = 1 border.border_width_bottom = 1 border.border_width_left = 1 for x in range(new_x): render_cells.append([]) for y in range(new_y): render_cells[x].append(Panel.new()) render_cells[x][y].name = str(x) + " " + str(y) render_cells[x][y].add_theme_stylebox_override("panel",border) render_cells[x][y].size.x = 20*$"case_size".value render_cells[x][y].size.y = 20*$"case_size".value render_cells[x][y].position.x = render_cells[x][y].size.x*x + offset[$"case_size".value][0] render_cells[x][y].position.y = render_cells[x][y].size.y*y + offset[$"case_size".value][1] $"maze".add_child(render_cells[x][y]) func update_cell(x,y,state): var update = render_cells[x][y].get_theme_stylebox("panel").duplicate() var cell_type = {0:Color("303030"),1:Color("ff0000"),2:Color("00ff00"),3:Color("ffffff"),4:Color("800080"),5:Color("0000ff")} update.bg_color = cell_type[state] render_cells[x][y].add_theme_stylebox_override("panel",update) if Vector2(x,y) == get_tooltip_position(): update_tooltip(-1,-1) func get_cell_state(x,y): if x < 0 or x > render_cells.size()-1 or y < 0 or y > render_cells[x].size()-1: return -1 var state = render_cells[x][y].get_theme_stylebox("panel").bg_color # 5: Visual only, 3-way intersection # 4: Visual only, openable wall cell # 3: Visual only, available cell # 2: Visited cell # 1: Unopenable wall cell # 0: Free cell var cell_type = {Color("303030"):0,Color("ff0000"):1,Color("00ff00"):2,Color("ffffff"):3,Color("800080"):4,Color("0000ff"):5} return cell_type[state] func get_tooltip_position(): return Vector2(int($"Tooltip/position/x".text) - 1,int($"Tooltip/position/y".text) - 1) func update_tooltip(x,y): var update_only = (x == -1 and y == -1) var inside_maze_renderer = (x >= $"maze".position.x and x < $"maze".size.x+$"maze".position.x) and (y >= $"maze".position.y and y < $"maze".size.y+$"maze".position.y) var relative_position = Vector2(x - $"maze".position.x - offset[$"case_size".value][0] , y - $"maze".position.y - offset[$"case_size".value][1]) var inside_case = (relative_position.x > 0 and relative_position.y > 0) and (relative_position.x < $"maze".size.x - offset[$"case_size".value][0]*2 and relative_position.y < $"maze".size.y - offset[$"case_size".value][1]*2) if update_only or inside_maze_renderer: if update_only or inside_case: if !$"Tooltip".visible: $"Tooltip".visible = true var case if !update_only: $"Tooltip".position = Vector2(x,y) case = (relative_position/(20*$"case_size".value)).floor() $"Tooltip/position/x".text = str(case.x + 1) $"Tooltip/position/y".text = str(case.y + 1) else: case = Vector2(int($"Tooltip/position/x".text) - 1,int($"Tooltip/position/y".text) - 1) var resize = ["CASE_FREE", "CASE_CLOSED", "CASE_VISITED", "CASE_AVAILABLE", "CASE_WALL", "CASE_INTERSECTION"] var state = get_cell_state(case.x,case.y) if state == -1: $"Tooltip".size.x = 86 $"Tooltip/case_type".size.x = 73 $"Tooltip/case_type".size.y = 24 $"Tooltip/case_type".text = "" else: $"Tooltip/case_type".size.y = 43 $"Tooltip/case_type".text = resize[state] $"Tooltip".size.x = max(86,$"Tooltip/case_type".get_content_width() + 4*2) $"Tooltip/case_type".size.x = $"Tooltip".size.x if x+$"Tooltip".size.x > $"maze".position.x+$"maze".size.x: $"Tooltip".position.x -= $"Tooltip".size.x if y+$"Tooltip".size.y > $"maze".position.y+$"maze".size.y: $"Tooltip".position.y -= $"Tooltip".size.y time_spent = Time.get_ticks_msec() elif $"Tooltip".visible: $"Tooltip".visible = false time_spent = 0 elif $"Tooltip".visible: $"Tooltip".visible = false time_spent = 0 return true func update_progress(taken:float,remaining:float): $"Progress/bar".size.x = (taken/remaining)*1152 $"Progress/count".position.x = 1110 - $"Progress/text".get_content_width() - 3 $"Progress/count".text = str(taken) + "/" + str(remaining) func get_save_path(file_format,callback): var path = FileDialog.new() path.name = "SAVE" path.file_mode = FileDialog.FILE_MODE_SAVE_FILE path.access = FileDialog.ACCESS_FILESYSTEM path.filters = PackedStringArray(["*." + file_format]) path.use_native_dialog = true path.file_selected.connect(callback) path.visible = true add_child(path) func write_save_path(file_format,path,content): var shortest = 2 + file_format.length() + 1 # Werewolves are on your way if path.length() < shortest or\ # For know-it-all users path.right(shortest-1) == "\\." + file_format or\ # For normal users path.right(shortest-2) != "." + file_format: path += "." + file_format var file = FileAccess.open(path,FileAccess.WRITE) file.store_string(content) func maze_to_CSV(): get_save_path("csv",save_CSV) func maze_to_JS(): get_save_path("html",save_JS) func save_CSV(path): var CSV = "" for y in range(render_cells[0].size()): for x in range(render_cells.size()): CSV += str(get_cell_state(x,y)) + "," CSV = CSV.left(-1) + "\n" write_save_path("csv",path,CSV) func save_JS(path): var JS = ''' Maze replay

Maze replay

By Foxarc, 2024.

Speed: second(s).


Frame: 0/1

''' var JS_array = "[" JS_array += str(JS_animation[0]) + "," + str(JS_animation[1]) + ",[" for x in range(JS_animation[2].size()): JS_array += "[" for y in range(JS_animation[2][x].size()): JS_array += str(JS_animation[2][x][y]) + "," JS_array = JS_array.left(-1) + "]," JS_array = JS_array.left(-1) + "]," for i in range(3,JS_animation.size()): JS_array += str(JS_animation[i]) + "," JS_array = JS_array.left(-1) + "]" JS = JS.replace("JS_ANIMATION",JS_array) write_save_path("html",path,JS) func _on_surface_size_x_changed(value): rerender_cells(value,$"surface_size_y".value) func _on_surface_size_y_changed(value): rerender_cells($"surface_size_x".value,value) func _on_case_size_value_changed(value): $"surface_size_x".max_value = max_cases[value][0] $"surface_size_y".max_value = max_cases[value][1] $"Limitations/x/max".text = str(max_cases[value][0]) $"Limitations/y/max".text = str(max_cases[value][1]) rerender_cells($"surface_size_x".value,$"surface_size_y".value) func _on_record_toggled(toggled_on): if (toggled_on): $"Captions/Visualization box".visible = false $"Captions/Visualization text".visible = false else: $"Captions/Visualization box".visible = true $"Captions/Visualization text".visible = true func _on_generate_pressed(): if $"Language hint".visible: $"Language hint".visible = false if $"Export".visible: $"Export".visible = false JS_animation = [] $"Progress".visible = false if generating: generating = false $"Generate".text = "GENERATE" return true var generated = false # For know-it-all users if $"surface_size_x".value == 1: for y in range(render_cells[0].size()): update_cell(0,y,1) generated = true elif $"surface_size_y".value == 1: for x in range(render_cells.size()): update_cell(x,0,1) generated = true elif $"surface_size_x".value == 2 and $"surface_size_y".value == 2: update_cell(0,0,1) update_cell(1,0,1) update_cell(0,1,1) update_cell(1,1,1) generated = true elif $"surface_size_x".value == 2 or $"surface_size_y".value == 2: for x in range($"surface_size_x".value): for y in range(render_cells[x].size()): update_cell(x,y,1) generated = true elif $"surface_size_x".value >= 100 or $"surface_size_y".value >= 100: return "foxes will now attack u" if !generated: $"surface_size_x".editable = false $"surface_size_y".editable = false $"case_size".editable = false $"Generate".text = "CANCEL" $"record".disabled = true if $"record".button_pressed: generated = await generate_and_record(Vector2($"surface_size_x".value,$"surface_size_y".value),render_cells) else: var cells = await generate(Vector2($"surface_size_x".value,$"surface_size_y".value),$"case_size".value) for x in range(render_cells.size()): for y in range(render_cells[x].size()): update_cell(x,y,cells[x][y]) generated = true $"surface_size_x".editable = true $"surface_size_y".editable = true $"case_size".editable = true $"Generate".text = "GENERATE" $"record".disabled = false if generated: if !JS_animation.size(): $"Export/JS".disabled = true $"Export/JS".text = "NO_JS" else: $"Export/JS".disabled = false $"Export/JS".text = "EXPORT_JS" $"Export".visible = true func _ready(): $"Captions/Visualization box".size.x = min(137 + $"Captions/3-way intersection text".get_content_width() + 3,293) $"Captions/Visualization text".position.x = $"Captions/Visualization box".position.x + $"Captions/Visualization box".size.x + 2 $"Captions/Visualization text".size.x = min($"Captions/Visualization text".get_content_width(),637) rerender_cells($"surface_size_x".value,$"surface_size_y".value) func _input(event): if event is InputEventMouseMotion: update_tooltip(event.position.x,event.position.y) func _process(_delta): if time_spent: if Time.get_ticks_msec() > time_spent + 1*1000: if Input.get_mouse_mode() == Input.MOUSE_MODE_VISIBLE: Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN) elif Input.get_mouse_mode() == Input.MOUSE_MODE_HIDDEN: Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) elif Input.get_mouse_mode() == Input.MOUSE_MODE_HIDDEN: Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) if Input.is_action_just_pressed("language"): language = (language + 1) % TranslationServer.get_loaded_locales().size() TranslationServer.set_locale(TranslationServer.get_loaded_locales()[language]) $"Captions/Visualization box".size.x = min(137 + $"Captions/3-way intersection text".get_content_width() + 3,293) $"Captions/Visualization text".position.x = $"Captions/Visualization box".position.x + $"Captions/Visualization box".size.x + 2 $"Captions/Visualization text".size.x = min($"Captions/Visualization text".get_content_width(),637) $"Progress/count".position.x = 1110 - $"Progress/text".get_content_width() - 3 update_tooltip(-1,-1) # Input: Surface size, Case size. # Output: Maze. func generate(floor_surface,size): size = 1 # Not now for a version 1 of the maze generator. var floor_size = floor_surface/size # Recursive backtracker algorithm using stack for low-performance computers # # From Wikipedia (06/29/2024): # Randomized depth-first search # This algorithm, also known as the "recursive backtracker" algorithm, is a randomized version of the depth-first search algorithm. # # Frequently implemented with a stack, this approach is one of the simplest ways to generate a maze using a computer. Consider the space for a # maze being a large grid of cells (like a large chess board), each cell starting with four walls. Starting from a random cell, the computer then # selects a random neighbouring cell that has not yet been visited. The computer removes the wall between the two cells and marks the new cell as # visited, and adds it to the stack to facilitate backtracking. The computer continues this process, with a cell that has no unvisited neighbours # being considered a dead-end. When at a dead-end it backtracks through the path until it reaches a cell with an unvisited neighbour, continuing # the path generation by visiting this new, unvisited cell (creating a new junction). This process continues until every cell has been visited, # causing the computer to backtrack all the way back to the beginning cell. We can be sure every cell is visited. # # As given above this algorithm involves deep recursion which may cause stack overflow issues on some computer architectures. The algorithm can be # rearranged into a loop by storing backtracking information in the maze itself. This also provides a quick way to display a solution, by starting # at any given point and backtracking to the beginning. # # Mazes generated with a depth-first search have a low branching factor and contain many long corridors, because the algorithm explores as far as # possible along each branch before backtracking. # # Iterative implementation (with stack) # # A disadvantage of the first approach is a large depth of recursion – in the worst case, # the routine may need to recur on every cell of the area being processed, which may exceed the maximum recursion stack depth in many environments. # As a solution, the same backtracking method can be implemented with an explicit stack, which is usually allowed to grow much bigger with no harm. # # 1. Choose the initial cell, mark it as visited and push it to the stack # 2. While the stack is not empty # 1. Pop a cell from the stack and make it a current cell # 2. If the current cell has any neighbours which have not been visited # 1. Push the current cell to the stack # 2. Choose one of the unvisited neighbours # 1. Remove the wall between the current cell and the chosen cell # 2. Mark the chosen cell as visited and push it to the stack # 2D cells var cells = [] # But first: # Our game is going to have its own rules for making the maze. Recall there is a distance between walls. # All algorithms will assume that the size of the walls around our cells doesn't matter at all. On the # contrary, recall there is a size for the floor of the backrooms. If we didn't consider the size for # the walls, we will find walls of a thickness of a paper and they will be out of touch for a backroom. # # Walls thickness must be considered: we do not need to make a new layer above the cells to mark the # walls around the cells but we can reuse the existing cells to mark walls around them that is for # each cell, there is exactly 8 cells of walls around it. It will make sense and won't cause a problem # to any algorithm in existence as we can simply skip the wall cell. # We will enclose the edges of the floor by walls. # For each cell: # - 0 means the cell is free # - 1 means the cell is a wall # - 2 means the cell was visited for x in range(floor_size.x): cells.append([]) cells[x].resize(floor_size.y) cells[x].fill(0) cells[x][0] = 1 # The first row will always be walls for y in range(1,floor_size.y-1): # Add walls on the edges of the floor cells[0][y] = 1 cells[floor_size.x-1][y] = 1 for x in range(floor_size.x): cells[x][floor_size.y-1] = 1 # We finish the last row with walls # How many cells are really available? We need another calculation for that. # Recall that each cell will have exactly 8 walls cells around it. Since there are 8 walls cells around a # cell, that means a cell takes a cell above and under, left and right for walls. # # 2 more rules: # 1. A cell is allowed to own the walls at the edges of the floor. # 2. A new cell will share another cell's walls if there are existing walls created by this previous cell. # # As a result, the first cell will have 8 walls around it, the next ones 8-3 = 5 walls from by sharing the # previous cell's walls, all the way up until the edge of the floor. var free_x = int(1 + (floor_size.x - 3)/2) # First cell will take 3 cases for walls, the next ones 2. var free_y = int(1 + (floor_size.y - 3)/2) # Ditto. # Apply the instructions from Wikipedia: var stack = [] # 1. Choose the initial cell, mark it as visited and push it to the stack var cur = [2*(randi() % free_x) + 1,2*(randi() % free_y) + 1] # Start with the cells, add +1 to find the free cell cells[cur[0]][cur[1]] = 2 stack.append(cur) # 2. While the stack is not empty while stack.size() != 0: # 1. Pop a cell from the stack and make it a current cell cur = stack.back() stack.pop_back() # 2. If the current cell has any neighbours which have not been visited var sides = [] if cur[1]-2 > -1 and cells[cur[0]][cur[1]-2] == 0: # Up sides.append([cur[0],cur[1]-2]) if cur[0]+2 < floor_size.x and cells[cur[0]+2][cur[1]] == 0: # Right sides.append([cur[0]+2,cur[1]]) if cur[1]+2 < floor_size.y and cells[cur[0]][cur[1]+2] == 0: # Down sides.append([cur[0],cur[1]+2]) if cur[0]-2 > -1 and cells[cur[0]-2][cur[1]] == 0: # Left sides.append([cur[0]-2,cur[1]]) if sides.size(): # 1. Push the current cell to the stack stack.append(cur) # 2. Choose one of the unvisited neighbours var next = sides[randi() % sides.size()] # 1. Remove the wall between the current cell and the chosen cell # 2. Mark the chosen cell as visited and push it to the stack cells[ cur[0] + (next[0]-cur[0])/2 ][ cur[1] + (next[1]-cur[1])/2 ] = 2 cells[next[0]][next[1]] = 2 stack.append(next) # When we are done with generating the maze, fill the remaining "free" cells with walls. for x in range(cells.size()): for y in range(cells[x].size()): if cells[x][y] == 0: cells[x][y] = 1 return cells # Input: Surface size, Case size, Render cells. func generate_and_record(floor_size,cells): var progress_speed = (1/floor_size.x + 1/floor_size.y) generating = true for x in range(floor_size.x): update_cell(x,0,1) if !generating: return false await get_tree().create_timer(progress_speed).timeout for y in range(1,floor_size.y-1): for x in range(floor_size.x): update_cell(x,y,0) update_cell(0,y,1) if !generating: return false await get_tree().create_timer(progress_speed).timeout update_cell(floor_size.x-1,y,1) if !generating: return false await get_tree().create_timer(progress_speed).timeout for x in range(floor_size.x): update_cell(x,floor_size.y-1,1) var free_x = int(1 + (floor_size.x - 3)/2) var free_y = int(1 + (floor_size.y - 3)/2) var free_cells = free_x*free_y var progress = 0 $"Progress".visible = true update_progress(progress,free_cells) var stack = [] # Mark available cells for visualization for x in range(free_x): for y in range(free_y): update_cell(2*x+1,2*y+1,3) if !generating: return false await get_tree().create_timer(progress_speed).timeout # Mark walls cells for visualization for x in range(1,cells.size()-1): for y in range(1,cells[x].size()-1): if y % 2 == 0: update_cell(x,y,4) elif 2*x < cells.size()-1 and y < cells[x].size()-1: update_cell(2*x,y,4) # Copy the untouched maze (preview) into JS animation JS_animation = [floor_size.x,floor_size.y,[]] for x in range(floor_size.x): JS_animation[2].append([]) for y in range(floor_size.y): JS_animation[2][x].append(get_cell_state(x,y)) var cur = [2*(randi() % free_x) + 1,2*(randi() % free_y) + 1] update_cell(cur[0],cur[1],2) progress += 1 update_progress(progress,free_cells) JS_animation.append(cur[0]) JS_animation.append(cur[1]) JS_animation.append(2) if !generating: return false await get_tree().create_timer(progress_speed).timeout stack.append(cur) while stack.size() != 0: cur = stack.back() stack.pop_back() var unchecked = [ [cur[0],cur[1]-2] , [cur[0]+2,cur[1]] , [cur[0],cur[1]+2] , [cur[0]-2,cur[1]] ] # Up, Right, Down, Left var sides = [] for pos in unchecked: if get_cell_state(pos[0],pos[1]) == 0 or get_cell_state(pos[0],pos[1]) == 3 or get_cell_state(pos[0],pos[1]) == 4: sides.append(pos) unchecked = [ [cur[0],cur[1]-1] , [cur[0]+1,cur[1]] , [cur[0],cur[1]+1] , [cur[0]-1,cur[1]] ] var connected = [] for pos in unchecked: if get_cell_state(pos[0],pos[1]) == 2 or get_cell_state(pos[0],pos[1]) == 5: connected.append(pos) if sides.size(): stack.append(cur) # We consider an intersection to be a cell with 3 sides connected to the cell if connected.size() == 2: # It is 2 as the algorithm always resumes from a +1 connected cell (2+1 = 3) update_cell(cur[0],cur[1],5) # Mark the cell as intersection for render only JS_animation.append(cur[0]) JS_animation.append(cur[1]) JS_animation.append(5) var next = sides[randi() % sides.size()] update_cell(cur[0] + (next[0]-cur[0])/2, cur[1] + (next[1]-cur[1])/2, 2) JS_animation.append(cur[0] + (next[0]-cur[0])/2) JS_animation.append(cur[1] + (next[1]-cur[1])/2) JS_animation.append(2) if !generating: return false await get_tree().create_timer(progress_speed).timeout update_cell(next[0],next[1],2) progress += 1 update_progress(progress,free_cells) JS_animation.append(next[0]) JS_animation.append(next[1]) JS_animation.append(2) if !generating: return false await get_tree().create_timer(progress_speed).timeout stack.append(next) for x in range(cells.size()): for y in range(cells[x].size()): if get_cell_state(x,y) == 0 or get_cell_state(x,y) == 4: update_cell(x,y,1) JS_animation.append(-1) JS_animation.append(-1) JS_animation.append(-1) generating = false return true
Free
Closed wall
Visited
Available
Wall
3-way intersection