Created
November 23, 2016 03:46
-
-
Save nick-paul/64abc82130563d5aa7011f38335a7aff to your computer and use it in GitHub Desktop.
A lua script for generating and solving mazes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --Maze Generator | |
| --Nicholas Paul | |
| -- ZeroBrane's turtle library | |
| require "turtle" | |
| math.randomseed(os.time()) | |
| SIZE = 100 --Width of tiles in the maze | |
| SCALE = 8 --Drawing Scale | |
| ---------------------------- | |
| -- Tile Object | |
| ---------------------------- | |
| WALL = true | |
| DOOR = false | |
| Tile = {} | |
| function Tile.new() | |
| local tile = {} | |
| tile.n = WALL | |
| tile.s = WALL | |
| tile.w = WALL | |
| tile.e = WALL | |
| tile.visited = false | |
| tile.on_frontier = false | |
| tile.is_available = true | |
| --tile.solve_state = NONE | |
| return tile | |
| end | |
| ---------------------------- | |
| -- Point | |
| ---------------------------- | |
| Point = {} | |
| function Point.new(x,y) | |
| local point = {} | |
| point.x = x | |
| point.y = y | |
| return point | |
| end | |
| ---------------------------- | |
| -- Frontier | |
| ---------------------------- | |
| local Frontier = {} | |
| Frontier.__index = Frontier | |
| --Add a tile to the frontier | |
| function Frontier.add(self, p) | |
| self[self.n+1] = p | |
| self.n = self.n+1 | |
| end | |
| --Return a random tile from the frontier and remove | |
| function Frontier.next(self) | |
| if self.n == 0 then | |
| io.write("frontier is empty") | |
| return nil | |
| end | |
| i = math.random(1, self.n) | |
| local p = self[i] | |
| --remove last element | |
| self[i] = self[self.n] | |
| self[self.n] = nil | |
| self.n = self.n - 1 | |
| return p | |
| end | |
| function Frontier.has_point(self) | |
| return self.n > 0 | |
| end | |
| ---------------------------- | |
| -- Util Functions | |
| ---------------------------- | |
| opposite_dir = {n = "s", s = "n", w = "e", e = "w"} | |
| --Return the x coordinate after moving in a specified direction | |
| function xmove(dir,x) | |
| local mv = {n = x-1, s = x+1, w = x, e = x} | |
| return mv[dir] | |
| end | |
| --Return the x coordinate after moving in a specified direction | |
| function ymove(dir,y) | |
| local mv = {n = y, s = y, w = y-1, e = y+1} | |
| return mv[dir] | |
| end | |
| function contains_point(points, x,y) | |
| for k,p in pairs(points) do | |
| if p.x == x and p.y == y then | |
| return true | |
| end | |
| end | |
| return false | |
| end | |
| --Generate a random available point on the maze | |
| function randpoint(maze) | |
| local good_tile = false | |
| local xr | |
| local yr | |
| while not good_tile do | |
| xr = math.random(SIZE) | |
| yr = math.random(SIZE) | |
| good_tile = maze[xr][yr].is_available | |
| end | |
| return Point.new(xr,yr) | |
| end | |
| --Create a deep copy of a table | |
| function deepcopy(orig) | |
| local orig_type = type(orig) | |
| local copy | |
| if orig_type == 'table' then | |
| copy = {} | |
| for orig_key, orig_value in next, orig, nil do | |
| copy[deepcopy(orig_key)] = deepcopy(orig_value) | |
| end | |
| setmetatable(copy, deepcopy(getmetatable(orig))) | |
| else -- number, string, boolean, etc | |
| copy = orig | |
| end | |
| return copy | |
| end | |
| ---------------------------- | |
| -- Filters | |
| ---------------------------- | |
| --remove a square from the center of the maze | |
| function sq_donut(map) | |
| local lower_q = math.ceil(SIZE*1/4) | |
| local upper_q = math.ceil(SIZE*3/4) | |
| for i = lower_q, upper_q do | |
| for j = lower_q, upper_q do | |
| map[i][j].is_available = false | |
| end | |
| end | |
| return map | |
| end | |
| --remove a circular hole in the center of the maze | |
| function rem_circ(map) | |
| local radius = SIZE/3 | |
| local center = math.ceil(SIZE/2) | |
| for i = 1,SIZE do | |
| for j = 1,SIZE do | |
| if math.sqrt( (center - i)^2 + (center-j)^2 ) < radius then | |
| map[i][j].is_available = false | |
| end | |
| end | |
| end | |
| return map | |
| end | |
| --Remove the outer edges of the maze to create a circle | |
| function circ(map) | |
| local radius = SIZE/2.1 | |
| local center = math.ceil(SIZE/2) | |
| for i = 1,SIZE do | |
| for j = 1,SIZE do | |
| if not (math.sqrt( (center - i)^2 + (center-j)^2 ) < radius) then | |
| map[i][j].is_available = false | |
| end | |
| end | |
| end | |
| return map | |
| end | |
| ---------------------------- | |
| -- Generate Maze | |
| ---------------------------- | |
| function generateMaze() | |
| --Maze is a 2D array of tiles | |
| local maze = {} | |
| for i=1,SIZE do | |
| maze[i] = {} | |
| for j=1,SIZE do | |
| maze[i][j] = Tile.new() | |
| end | |
| end | |
| --Apply a filter here | |
| --maze = sq_donut(maze) | |
| --maze = rem_circ(maze) | |
| maze = circ(maze) | |
| local frontier = setmetatable({}, Frontier) | |
| frontier.n = 0 --Number of elements in the frontier | |
| --Choose a statring point | |
| --Chose a point at random | |
| local good_tile = false | |
| local xr | |
| local yr | |
| while not good_tile do | |
| xr = math.random(SIZE) | |
| yr = math.random(SIZE) | |
| good_tile = maze[xr][yr].is_available | |
| end | |
| --set it to visited | |
| maze[xr][yr].visited = true | |
| --...and add it to the frontier | |
| frontier:add(Point.new(xr,yr)) | |
| --while the frontier still has elements, choose a random one | |
| --and iterate through the map | |
| while frontier:has_point() do | |
| --choose a random point in the frontier | |
| local p = frontier:next() | |
| --shortcuts | |
| local x = p.x | |
| local y = p.y | |
| --set current tile to visited | |
| maze[x][y].visited = true | |
| local visited = {} | |
| local unexplored = {} | |
| --if there exists an adjacent tile; if it has been visited and is available, add it to the visited list | |
| if x+1 <= SIZE and maze[x+1][y] ~= nil and maze[x+1][y].is_available then | |
| if maze[x+1][y].visited then table.insert(visited, "s") else table.insert(unexplored, Point.new(x+1,y)) end | |
| end | |
| if x-1 > 0 and maze[x-1][y] ~= nil and maze[x-1][y].is_available then | |
| if maze[x-1][y].visited then table.insert(visited, "n") else table.insert(unexplored, Point.new(x-1,y)) end | |
| end | |
| if y+1 <= SIZE and maze[x][y+1] ~= nil and maze[x][y+1].is_available then | |
| if maze[x][y+1].visited then table.insert(visited, "e") else table.insert(unexplored, Point.new(x,y+1)) end | |
| end | |
| if y-1 > 0 and maze[x][y-1] ~= nil and maze[x][y-1].is_available then | |
| if maze[x][y-1].visited then table.insert(visited, "w") else table.insert(unexplored, Point.new(x,y-1)) end | |
| end | |
| --Choose a visited tile and carve a doorway | |
| if #visited ~= 0 then | |
| local dir = visited[math.random(1, #visited)] | |
| maze[x][y][dir] = DOOR | |
| maze[xmove(dir,x)][ymove(dir,y)][opposite_dir[dir]] = DOOR | |
| end | |
| --add the unexploerd tiles to the frontier | |
| for k,v in pairs(unexplored) do | |
| if not maze[v.x][v.y].on_frontier then | |
| maze[v.x][v.y].on_frontier = true | |
| frontier:add(v) | |
| end | |
| end | |
| end | |
| return maze | |
| end | |
| ---------------------------- | |
| -- Dwarf | |
| ---------------------------- | |
| Dwarf = {} --Dwarfs are great at solving mazes | |
| Dwarf.__index = Dwarf | |
| setmetatable(Dwarf, { | |
| __call = function (s, ...) | |
| return s.new(...) | |
| end, | |
| }) | |
| -- takes the route of the dwarf that spawned it and the current point it is at | |
| function Dwarf.new(route, point, dir_from) | |
| local self = setmetatable({}, Dwarf) | |
| if route == nil then | |
| self.route = {} | |
| else | |
| self.route = deepcopy(route) | |
| end | |
| self.loc = deepcopy(point) | |
| table.insert(self.route, self.loc) | |
| self.dir_came_through = dir_from | |
| return self | |
| end | |
| ---------------------------- | |
| -- Solve Maze | |
| ---------------------------- | |
| --Return an array of Points indicating the solution path | |
| --map::Maze, enterance::Point, exit::Point | |
| function sln_path(map, entrance, exit) | |
| local solved = false | |
| local dwarves = {} | |
| table.insert(dwarves, Dwarf({}, entrance, "start")) | |
| while true do | |
| local newdwarves = {} | |
| --Loop through the dwarves and have them spawn more dwarves | |
| for i,df in pairs(dwarves) do | |
| --Has the dwarf made it to the exit? | |
| if df.loc.x == exit.x and df.loc.y == exit.y then | |
| return df.route | |
| end | |
| --Are there nearby doors | |
| local dirs = {"n", "s", "e", "w"} | |
| for k,dir in pairs(dirs) do | |
| --if this is not the direction the dwarf came through and it is a door, spawn a child dwarf in that tile | |
| if (df.dir_came_through ~= dir) | |
| and (maze[df.loc.x][df.loc.y][dir] == DOOR) | |
| and (maze[xmove(dir, df.loc.x)][ymove(dir, df.loc.y)] ~= nil) then | |
| --If there exists a tile in the potential location, spwan a dwarf there | |
| if (maze[xmove(dir, df.loc.x)][ymove(dir, df.loc.y)].is_available) then | |
| table.insert(newdwarves, Dwarf(df.route, Point.new(xmove(dir, df.loc.x), ymove(dir, df.loc.y)), opposite_dir[dir])) | |
| end | |
| end | |
| end | |
| end | |
| dwarves = newdwarves | |
| end | |
| end | |
| --Draw the maze onto the canvas | |
| function draw_maze(maze, spath) | |
| local hypot = math.sqrt(SCALE*SCALE + SCALE*SCALE) | |
| --Initialize the turtle (move to top left) | |
| size(SIZE*SCALE+1, SIZE*SCALE+1) | |
| turn(180); jump(SIZE*SCALE/2) | |
| turn(90); jump(SIZE*SCALE/2-SCALE) | |
| turn(90) | |
| --draw the tiles | |
| for i = 1,SIZE do | |
| for j = 1,SIZE do | |
| jump(SCALE) | |
| if maze[i][j].is_available then | |
| turn(180) --face left | |
| if maze[i][j].s then move(SCALE) else jump(SCALE) end --south wall | |
| turn(90) --face up | |
| if maze[i][j].w then move(SCALE) else jump(SCALE) end --west wall | |
| turn(90) --face right | |
| if maze[i][j].n then move(SCALE) else jump(SCALE) end --north wall | |
| turn(90) --face down | |
| if maze[i][j].e then move(SCALE) else jump(SCALE) end --east wall | |
| turn(-90) --face right | |
| if contains_point(spath, i, j) then | |
| turn(-1*(90+45)) --face NW | |
| jump(hypot/2) | |
| turn(180) --face SE | |
| --Draw a crumb in the tile | |
| turn(-45) --face right | |
| move(2); turn(90) | |
| move(1); turn(90) | |
| move(2); turn(90) | |
| move(1); turn(90) | |
| move(2); turn(180) | |
| jump(2); turn(-1*(90+45)) | |
| jump(hypot/2) | |
| turn(-45) --face right | |
| end | |
| end | |
| end | |
| turn(180) --face left | |
| jump(SIZE*SCALE) | |
| turn(-90) --face down | |
| jump(SCALE) | |
| turn(-90) --face right | |
| end | |
| end | |
| --Generate a new maze | |
| maze = generateMaze() | |
| --Generate a path between two random points | |
| --spath = sln_path(maze, randpoint(maze), randpoint(maze)) | |
| spath = sln_path(maze, Point.new(SIZE/2, 3), Point.new(SIZE/2, SIZE-3)) | |
| --Generate a path from the top left to the lower right corners | |
| --spath = sln_path(maze, Point.new(1,1), Point.new(SIZE,SIZE)) | |
| --Draw the maze | |
| draw_maze(maze, spath) | |
| wait() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment