1- {-# LANGUAGE TemplateHaskell #-}
1+ {-# LANGUAGE TemplateHaskell, GeneralizedNewtypeDeriving #-}
22
33module AStarSearchTest where
44
55import AStarSearch
6+ import Data.Hashable (Hashable )
67import Data.List (foldl' )
7- import Data.Maybe (fromJust )
8+ import Data.Maybe (fromJust , fromMaybe )
89import Data.Sequence ((|>) )
910import Test.QuickCheck
1011import Test.QuickCheck.All
@@ -14,24 +15,24 @@ import qualified Data.Sequence as Seq
1415-- We use A* search to find the shortest path (path with least number of moves) of a knight
1516-- from a start square to a goal square on a chess board.
1617
17- type Square = (Int , Int )
18+ newtype Square = Square (Int , Int ) deriving ( Eq , Hashable , Show )
1819type Path = [Square ]
1920
2021-- Finds the next squares a knight can move to from a given square
21- nextKnightPos (x, y) =
22- filter isValidMove . map (\ (dx, dy) -> (x + dx, y + dy)) $ moves
22+ nextKnightPos (Square ( x, y) ) =
23+ map Square . filter isValidMove . map (\ (dx, dy) -> (x + dx, y + dy)) $ moves
2324 where
2425 moves = [(1 ,2 ), (1 ,- 2 ), (- 1 ,2 ), (- 1 ,- 2 ), (2 ,1 ), (2 ,- 1 ), (- 2 ,1 ), (- 2 ,- 1 )]
2526 isValidMove (x, y) = and [x > 0 , x < 9 , y > 0 , y < 9 ]
2627
2728-- Creates the heuristic function given a goal square. The heuristic used is half of the max of
2829-- the distance between x coordinates and the distance between y coordinates.
29- mkHeuristic (gx, gy) ( x, y) = max (abs (x- gx)) (abs (y- gy)) `div` 2
30+ mkHeuristic (Square ( gx, gy)) ( Square ( x, y) ) = max (abs (x- gx)) (abs (y- gy)) `div` 2
3031
31- -- Finds the shortest path of the knight
32+ -- Finds the shortest path of the knight, returns empty path if the goal is invalid
3233knightsShortestPath :: Square -> Square -> Path
3334knightsShortestPath initSq goalSq =
34- snd . fromJust
35+ snd . fromMaybe ( 0 , [] )
3536 $ astarSearch initSq (== goalSq) (flip zip (repeat 1 ) . nextKnightPos) (mkHeuristic goalSq)
3637
3738-- Finds the shortest path using breadth first search. Used for checking if the path returned by
@@ -40,7 +41,7 @@ bfs :: Square -> Square -> Path
4041bfs startSq goalSq =
4142 bfs' goalSq (Map. singleton startSq noSuchSq) (Seq. singleton startSq)
4243 where
43- noSuchSq = (- 1 ,- 1 )
44+ noSuchSq = Square (- 1 , - 1 )
4445
4546 bfs' goalSq tracks open = let
4647 (first, rest) = Seq. splitAt 1 open
@@ -58,30 +59,30 @@ bfs startSq goalSq =
5859 else []
5960
6061-- Setup to generate arbitrary squares for testing
61- newtype TestSquare = TestSquare { sq :: Square } deriving (Show )
62-
63- instance Arbitrary TestSquare where
62+ instance Arbitrary Square where
6463 arbitrary = do
6564 x <- choose (1 , 8 )
6665 y <- choose (1 , 8 )
67- return $ TestSquare (x, y)
66+ return $ Square (x, y)
6867
6968-- Properties to test
7069prop_path_starts_with_start_square startSq goalSq =
71- head (knightsShortestPath (sq startSq) (sq goalSq)) == sq startSq
70+ head (knightsShortestPath startSq goalSq) == startSq
7271
7372prop_path_ends_with_goal_square startSq goalSq =
74- last (knightsShortestPath (sq startSq) (sq goalSq)) == sq goalSq
73+ last (knightsShortestPath startSq goalSq) == goalSq
7574
7675prop_path_consists_of_valid_knights_moves startSq goalSq =
77- let path = knightsShortestPath (sq startSq) (sq goalSq)
76+ let path = knightsShortestPath startSq goalSq
7877 in all isValidKnightsMove $ zip path (tail path)
7978 where
8079 isValidKnightsMove (sqFrom, sqTo) = sqTo `elem` nextKnightPos sqFrom
8180
81+ prop_no_path_for_invalid_goal startSq =
82+ knightsShortestPath startSq (Square (- 1 , - 1 )) == []
83+
8284prop_path_is_shortest startSq goalSq =
83- let path = knightsShortestPath (sq startSq) (sq goalSq)
84- in length path == length (bfs (sq startSq) (sq goalSq))
85+ length (knightsShortestPath startSq goalSq) == length (bfs startSq goalSq)
8586
8687-- Tests all the properties 1000 times each
8788testAllProps = $ forAllProperties $ quickCheckWithResult (stdArgs {maxSuccess = 1000 })
0 commit comments