rubyquiz/AmazingMazes.hs

188 lines
7.0 KiB
Haskell
Raw Normal View History

2012-10-27 13:36:30 +05:30
{-|
A solution to rubyquiz 31 (<http://rubyquiz.com/quiz31.html>).
2012-09-19 23:47:45 +05:30
2012-10-27 13:36:30 +05:30
/Generate a rectangular maze given its width and height. The maze should be/
/solvable for any start and end positions and there should be only one possible/
/solution for any pair of start and end positions./
2012-09-19 23:47:45 +05:30
2012-10-27 13:36:30 +05:30
/Generate the ASCII output representing the maze./
2012-09-19 23:47:45 +05:30
2012-10-27 13:36:30 +05:30
/Find the solution of the maze. Produce ASCII output to visualize the solution./
2012-09-19 23:59:37 +05:30
The maze generation algorithm used is recursive backtracking and the maze
2012-10-27 13:36:30 +05:30
solution algorithm used is A* (from "AStar" module).
Usage (Coordinates are zero based):
> ./AmazingMazes <width> <height> <start_x> <start_y> <end_x> <end_y>
Example:
> abhinav@xj9:rubyquiz# bin/AmazingMazes 10 10 0 0 9 9
> +---+---+---+---+---+---+---+---+---+---+
> | s > v | | |
> +---+---+ +---+ + +---+ + +---+
> | v < < | | | | | |
> + +---+---+---+---+---+ + + + +
> | v | | | |
> + +---+---+---+ + +---+---+---+ +
> | > > > v | | | > v |
> +---+---+---+ + + +---+---+ + +
> | | v < | | > > ^ | v |
> + + + +---+---+---+ +---+---+ +
> | | | > > > > ^ | | v |
> + +---+---+---+---+---+---+ +---+ +
> | | | | v < < |
> + + + + + + +---+ +---+---+
> | | | | | | v < | |
> + + +---+---+ +---+ +---+ +---+
> | | | | v < | |
> + +---+ + + + +---+---+---+ +
> | | | > > > > e |
> +---+---+---+---+---+---+---+---+---+---+
Copyright 2012 Abhinav Sarkar \<abhinav\@abhinavsarkar.net\>
2012-09-19 23:47:45 +05:30
-}
{-# LANGUAGE BangPatterns, TupleSections #-}
2012-10-27 11:07:22 +05:30
module AmazingMazes (Cell(..), Maze(..), MazeSolution(..),
generateMaze, renderMaze, solveMaze, main)
where
2012-09-19 23:47:45 +05:30
import qualified Data.Map as M
import AStar
import Data.List (nub)
import Data.Maybe (fromMaybe)
import Control.Monad (foldM)
import Control.Monad.State (State, get, put, evalState)
import System.Environment (getArgs)
import System.Random (Random, StdGen, randomR, randomRs, newStdGen, split)
sliding :: Int -> Int -> [a] -> [[a]]
sliding _ _ [] = []
sliding size step xs = take size xs : sliding size step (drop step xs)
-- randomness --
type RandomState = State StdGen
getRandomR :: Random a => (a, a) -> RandomState a
getRandomR limits = do
gen <- get
let (val, gen') = randomR limits gen
put gen'
return val
getRandomRs :: Random a => (a, a) -> RandomState [a]
getRandomRs limits = do
gen <- get
return $ randomRs limits gen
randomShuffle :: [a] -> RandomState [a]
randomShuffle list = do
let len = length list
rs <- getRandomRs (0, len - 1)
g <- get
let (_, g') = split g
put g'
return $ map (list !!) . head . dropWhile ((/= len) . length) . map nub . sliding len 1 $ rs
-- maze --
2012-10-27 13:36:30 +05:30
-- | A cell with x and y coordinates
2012-09-19 23:47:45 +05:30
type Cell = (Int, Int)
2012-10-27 13:36:30 +05:30
-- | A maze with width, height and a map of cell paths
2012-10-27 11:07:22 +05:30
data Maze = Maze Int Int (M.Map Cell [Cell])
2012-09-19 23:47:45 +05:30
2012-10-27 13:36:30 +05:30
-- | A solution to a maze with the start and end cells and the path map
2012-09-19 23:47:45 +05:30
data MazeSolution = MazeSolution Cell Cell (M.Map Cell Cell)
2012-10-27 13:36:30 +05:30
-- | Gets the neighbour cells
2012-09-19 23:47:45 +05:30
nextCells :: Int -> Int -> Cell -> [Cell]
nextCells width height (x, y) =
filter (\(x', y') -> and [x' >= 0, x' < width, y' >= 0, y' < height])
. map (\(xd, yd) -> (x + xd, y + yd))
$ [(0,-1), (1,0), (0,1), (-1,0)]
2012-10-27 13:36:30 +05:30
-- | Generates a random maze given the start cell and an empty maze
2012-09-19 23:47:45 +05:30
generateMaze_ :: Cell -> Maze -> RandomState Maze
generateMaze_ start maze@(Maze width height cellMap) = do
!next <- randomShuffle . filter (not . flip M.member cellMap) $ nextCells width height start
if null next
then return $ Maze width height (M.insertWith' (++) start [] cellMap)
else
2012-09-19 23:59:37 +05:30
foldM (\mz@(Maze _ _ m) n -> M.keys m `seq`
2012-09-19 23:47:45 +05:30
if not . M.member n $ m
then generateMaze_ n
(Maze width height
(M.insertWith' (++) n [start] (M.insertWith' (++) start [n] m)))
else return mz)
maze next
2012-10-27 13:36:30 +05:30
-- | Generates a random maze given the maze width and height using recursive backtracking
generateMaze :: Int -- ^ Maze width
-> Int -- ^ Maze height
-> State StdGen Maze -- ^ The generated maze inside a 'State' monad with a random generator
2012-09-19 23:47:45 +05:30
generateMaze width height = do
x <- getRandomR (0, width - 1)
y <- getRandomR (0, height - 1)
generateMaze_ (x, y) (Maze width height M.empty)
2012-10-27 13:36:30 +05:30
-- | Renders a maze and its solution as a string
2012-09-19 23:47:45 +05:30
renderMaze :: Maze -> MazeSolution -> String
renderMaze maze@(Maze width height _) solution =
concatMap (renderMazeRow maze solution) [0 .. (height - 1)]
++ concat (replicate width "+---") ++ "+"
2012-10-27 13:36:30 +05:30
-- | Renders a row of a maze and the maze's solution as a string
2012-09-19 23:47:45 +05:30
renderMazeRow :: Maze -> MazeSolution -> Int -> String
renderMazeRow maze@(Maze width height _) solution rowIx =
let (up, side) = unzip . map (renderMazeCell maze solution rowIx) $ [0 .. (width - 1)]
in concat up ++ "+" ++ "\n" ++ concat side ++ "|" ++ "\n"
2012-10-27 13:36:30 +05:30
-- | Renders a cell of a maze and the maze's solution as a pair of strings
2012-09-19 23:47:45 +05:30
renderMazeCell :: Maze -> MazeSolution -> Int -> Int -> (String, String)
renderMazeCell (Maze _ _ cellMap) (MazeSolution start end solution) rowIx colIx = let
cell = (colIx, rowIx)
up = (colIx, rowIx - 1)
side = (colIx - 1, rowIx)
in ("+" ++ if up `elem` next cell then " " else "---",
(if side `elem` next cell then " " else "|") ++ " " ++ mark cell ++ " ")
where
next = fromMaybe [] . flip M.lookup cellMap
mark cell@(x, y)
| cell == start = "s"
| cell == end = "e"
| otherwise = case M.lookup cell solution of
Nothing -> " "
Just (x', y') -> fromMaybe " " $ M.lookup (x' - x, y' - y) marks
2012-10-27 13:36:30 +05:30
-- | Symbols to mark the solution path
marks = M.fromList [((0,-1), "^"), ((1,0), ">"), ((0,1), "v"), ((-1,0), "<")]
2012-09-19 23:47:45 +05:30
2012-10-27 13:36:30 +05:30
-- | Solves the maze using A* given the maze and the start and end cells using
2012-09-19 23:59:37 +05:30
-- Manhattan distance as the heuristic
2012-10-27 13:36:30 +05:30
solveMaze :: Maze -- ^ The maze to solve
-> Cell -- ^ The start cell
-> Cell -- ^ The end cell
-> MazeSolution -- ^ The solution of the maze
2012-09-19 23:47:45 +05:30
solveMaze maze@(Maze _ _ cellMap) start end =
MazeSolution start end
. M.fromList
. map (\a -> (a !! 0, a !! 1))
. filter ((== 2) . length)
. sliding 2 1
. fromMaybe [] . fmap snd
2012-09-19 23:59:37 +05:30
. astar start end (map (,1) . fromMaybe [] . flip M.lookup cellMap)
2012-09-19 23:47:45 +05:30
$ (\(x, y) (x', y') -> abs (x - x') + abs (y - y'))
2012-10-27 13:36:30 +05:30
-- | Reads the width, height, start and end cell co-ordinates from command
-- line arguments, generates a maze using them, solves it and renders it
-- with the solution.
2012-09-19 23:47:45 +05:30
main = do
(width : height : sx : sy : ex : ey : _) <- fmap (map read) getArgs
g <- newStdGen
let mz = evalState (generateMaze width height) g
putStrLn $ renderMaze mz (solveMaze mz (sx, sy) (ex, ey))