1+ {-# LANGUAGE TemplateHaskell #-}
2+
13module AStarSearchTest where
24
35import AStarSearch
6+ import Data.List (foldl' )
47import Data.Maybe (fromJust )
8+ import Data.Sequence ((|>) )
9+ import Test.QuickCheck
10+ import Test.QuickCheck.All
11+ import qualified Data.HashMap.Strict as Map
12+ import qualified Data.Sequence as Seq
513
614-- We use A* search to find the shortest path (path with least number of moves) of a knight
715-- from a start square to a goal square on a chess board.
816
17+ type Square = (Int , Int )
18+ type Path = [Square ]
19+
920-- Finds the next squares a knight can move to from a given square
1021nextKnightPos (x, y) =
11- zip ( filter isValidMove . map (\ (dx, dy) -> (x + dx, y + dy)) $ moves) ( repeat 1 )
12- where
22+ filter isValidMove . map (\ (dx, dy) -> (x + dx, y + dy)) $ moves
23+ where
1324 moves = [(1 ,2 ), (1 ,- 2 ), (- 1 ,2 ), (- 1 ,- 2 ), (2 ,1 ), (2 ,- 1 ), (- 2 ,1 ), (- 2 ,- 1 )]
1425 isValidMove (x, y) = and [x > 0 , x < 9 , y > 0 , y < 9 ]
1526
@@ -18,23 +29,62 @@ nextKnightPos (x, y) =
1829mkHeuristic (gx, gy) (x, y) = max (abs (x- gx)) (abs (y- gy)) `div` 2
1930
2031-- Finds the shortest path of the knight
21- knightsShortestPath :: ( Int , Int ) -> ( Int , Int ) -> [( Int , Int )]
32+ knightsShortestPath :: Square -> Square -> Path
2233knightsShortestPath initSq goalSq =
23- snd . fromJust $ astarSearch initSq (== goalSq) nextKnightPos (mkHeuristic goalSq)
24-
25- -- example runs
26- main :: IO ()
27- main = do
28- print $ knightsShortestPath (1 ,1 ) (2 ,2 )
29- -- prints [(1,1),(3,2),(5,3),(4,1),(2,2)]
30- print $ knightsShortestPath (4 ,1 ) (7 ,2 )
31- -- prints [(4,1),(5,3),(7,2)]
32- print $ knightsShortestPath (4 ,1 ) (8 ,7 )
33- -- prints [(4,1),(6,2),(5,4),(7,5),(8,7)]
34- print $ knightsShortestPath (8 ,1 ) (7 ,8 )
35- -- prints [(8,1),(7,3),(8,5),(6,6),(7,8)]
36- print $ knightsShortestPath (1 ,1 ) (8 ,8 )
37- -- prints [(1,1),(2,3),(3,5),(4,3),(5,5),(7,6),(8,8)]
38- print $ knightsShortestPath (1 ,1 ) (1 ,1 )
39- -- prints [(1,1)]
40- return ()
34+ snd . fromJust
35+ $ astarSearch initSq (== goalSq) (flip zip (repeat 1 ) . nextKnightPos) (mkHeuristic goalSq)
36+
37+ -- Finds the shortest path using breadth first search. Used for checking if the path returned by
38+ -- A* search is indeed shortest.
39+ bfs :: Square -> Square -> Path
40+ bfs startSq goalSq =
41+ bfs' goalSq (Map. singleton startSq noSuchSq) (Seq. singleton startSq)
42+ where
43+ noSuchSq = (- 1 ,- 1 )
44+
45+ bfs' goalSq tracks open = let
46+ (first, rest) = Seq. splitAt 1 open
47+ currentSq = Seq. index first 0
48+ in if currentSq == goalSq
49+ then consPath currentSq
50+ else let
51+ nextSqs = filter (not . flip Map. member tracks) . nextKnightPos $ currentSq
52+ tracks' = foldl' (\ t s -> Map. insert s currentSq t) tracks nextSqs
53+ in bfs' goalSq tracks' (foldl (|>) rest nextSqs)
54+ where
55+ consPath square =
56+ if Map. member square tracks
57+ then consPath (fromJust . Map. lookup square $ tracks) ++ [square]
58+ else []
59+
60+ -- Setup to generate arbitrary squares for testing
61+ newtype TestSquare = TestSquare { sq :: Square } deriving (Show )
62+
63+ instance Arbitrary TestSquare where
64+ arbitrary = do
65+ x <- choose (1 , 8 )
66+ y <- choose (1 , 8 )
67+ return $ TestSquare (x, y)
68+
69+ -- Properties to test
70+ prop_path_starts_with_start_square startSq goalSq =
71+ head (knightsShortestPath (sq startSq) (sq goalSq)) == sq startSq
72+
73+ prop_path_ends_with_goal_square startSq goalSq =
74+ last (knightsShortestPath (sq startSq) (sq goalSq)) == sq goalSq
75+
76+ prop_path_consists_of_valid_knights_moves startSq goalSq =
77+ let path = knightsShortestPath (sq startSq) (sq goalSq)
78+ in all isValidKnightsMove $ zip path (tail path)
79+ where
80+ isValidKnightsMove (sqFrom, sqTo) = sqTo `elem` nextKnightPos sqFrom
81+
82+ prop_path_is_shortest startSq goalSq =
83+ let path = knightsShortestPath (sq startSq) (sq goalSq)
84+ in length path == length (bfs (sq startSq) (sq goalSq))
85+
86+ -- Tests all the properties 1000 times each
87+ testAllProps = $ forAllProperties $ quickCheckWithResult (stdArgs {maxSuccess = 1000 })
88+
89+ -- main function runs the tests. Type `runhaskell AStarSearch_test.hs` on command line to run the tests.
90+ main = testAllProps >> return ()
0 commit comments