-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathMazeGenerator.hs
More file actions
148 lines (124 loc) · 6.65 KB
/
MazeGenerator.hs
File metadata and controls
148 lines (124 loc) · 6.65 KB
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
{-# LANGUAGE RecordWildCards #-}
{-# OPTIONS_GHC -Wno-x-partial #-}
module MazeGenerator where
import RandomGeneration
import Types
import Prelude hiding (rem)
import Control.Concurrent
import qualified Data.Set as S
{-
Source:
http://en.wikipedia.org/wiki/Maze_generation#Recursive_backtracker
The depth-first search algorithm of maze generation is frequently
implemented using backtracking:
Make the initial cell the current cell and mark it as visited
While there are unvisited cells
If the current cell has any unvisited neighbours -- see (Note: 1)
Choose randomly one of the unvisited neighbours
Push the current cell to the stack
Remove the wall between the current cell and the chosen cell
Make the chosen cell the current cell and mark it as visited
Else if stack is not empty -- see (Note: 2)
Pop a cell from the stack
Make it the current cell
Else -- see (Note: 3)
Pick a random unvisited cell, make it the current cell and
mark it as visited
-}
-- tail-recursive implementation of Depth-first search algorithm
-- * buildSnapshot must announce a visual update of the generation process
-- * randomFunc must make a random pick which is GenerationBias-aware
depthFirstSearch
:: ([MazeIx] -> IO ())
-> (MazeIx -> [MazeIx] -> IO MazeIx)
-> (MazeIx -> [MazeIx])
-> [MazeIx]
-> Int
-> IO [MazeIx]
depthFirstSearch buildSnapshot randomFunc neighboursAround maze_ =
depthFirstSearch' maze_ S.empty (0, 0) [] -- (0, 0) with empty stack results in a random starting point
where
emptyCells = S.fromList maze_
depthFirstSearch' maze _ _ _ 0 = return maze -- no remaining unvisited cells? done!
depthFirstSearch' maze visit current@(cx, cy) stack rem =
let
unvisitedNeighbs =
[ix | ix <- neighboursAround current, not (S.member ix visit)]
(visit', rem') -- adjust remaining unvisited cell count and mark current cell as visited if necessary
| S.member current visit = (visit, rem)
| otherwise = (S.insert current visit, rem-1)
in if null unvisitedNeighbs
then case stack of
[] -> do
next <- randomElement $
S.toList (S.difference emptyCells visit) -- all unvisited cells is the set difference between empty and visited cells
depthFirstSearch' maze visit' next stack rem' -- Note: 3
c:cs ->
depthFirstSearch' maze visit' c cs rem' -- Note: 2
else do
next@(nx, ny) <- randomFunc current unvisitedNeighbs
let
tearDown = ((cx+nx) `div` 2, (cy+ny) `div` 2)
maze' = tearDown:maze
buildSnapshot maze' -- things have changed; give an update to whom it may concern
depthFirstSearch' maze' visit' next (current:stack) rem' -- Note: 1
-- (possibly animated) generation of a new maze conforming to the
-- parameters held in the application state.
generateMaze :: MVar AppState -> IO ()
generateMaze appState = do
AppState {..} <- readMVar appState
let
-- the neighboring empty maze cells
neighboursAround (x, y) =
filter clipping [(x-2, y), (x+2, y), (x, y-2), (x, y+2)]
where
clipping (i, j) =
i > 0 && j > 0 && i < 2 * fst asDims && j < 2 * snd asDims
-- visualizes the generation process; increase delay to slow down the animation
buildSnapshot xs = do
asRenderFrame $ RenderFrame asQuadWH Nothing xs
threadDelay 2000
modifyAppState appState $ \st -> st {asNeedBuild = False, asAnimating = True, asSolution = Nothing}
maze <- depthFirstSearch
(if asShowBuild then buildSnapshot else const (pure ()))
(randomBias asBuildBias asDims)
neighboursAround
(emptyMaze asDims)
(uncurry (*) asDims)
modifyAppState appState $ \st -> st {asMaze = maze, asAnimating = False}
asRenderFrame $ RenderFrame asQuadWH Nothing maze
-- animates the algorithm that solves the current maze on display.
-- the solver keeps track of all paths it is walking
-- simultaneously as a list of paths. it tries to extend
-- each path with adjacent unused maze indices. a path which can't be
-- extended further has either hit the exit, or it's removed from the list.
-- NB. this algorithm can't solve mazes with circular paths.
solveMaze :: MVar AppState -> IO ()
solveMaze appState = do
AppState{..} <- readMVar appState
let
enter = (0, 1) -- punch a hole in the wall bottom left...
exit = let (right, upper) = maximum asMaze in (right+1, upper) -- ...and top right
maze = exit : asMaze
doesExit = dropWhile ((/= exit) . head) -- look for the first path to hit the exit
moves (x, y) = [(x+1, y), (x-1, y), (x, y+1), (x, y-1)]
solveRecursive free sols
| S.null free || null sols = pure Nothing -- conditions on which a maze is unsolvable
| otherwise = do
let
sols' = concat [ map (:sol) ms
| sol@(s:_) <- sols
, let ms = filter (`S.member` free) $ moves s
, (not . null) ms
]
free' = foldr (S.delete . head) free sols' -- remove recent path extensions from the Set
reds = (S.toList . S.fromList . concat) sols' -- dedupe for drawing only; completely unoptimized
asRenderFrame $ RenderFrame asQuadWH (Just reds) maze
threadDelay 7500 -- increase delay to slow down the animation
case doesExit sols' of
x:_ -> pure $ Just x
_ -> solveRecursive free' sols'
modifyAppState appState $ \st -> st {asNeedSolve = False, asAnimating = True, asSolution = Nothing}
sol <- solveRecursive (S.fromList maze) [[enter]]
modifyAppState appState $ \st -> st {asSolution = sol, asAnimating = False}
asRenderFrame $ RenderFrame asQuadWH sol maze