I'm trying to do a property-based test for a chess game. I have set up the following typeclass
class Monad m => HasCheck m where
isCollision :: Coord -> m Bool
which checks if a given coordinate contains a collision or out of bounds.
Now I have a function that generates the moveset of allowed actions for a knight like the following
collisionKnightRule :: HasCheck m => Coord -> m (Set Coord)
collisionKnightRule =
Set.filterM isCollision . knightMoveSet
-- | Set of all moves, legal or not
knightMoveSet :: Coord -> Set Coord
knightMoveSet (x,y) =
Set.fromList
[ (x+2,y-1),(x+2,y+1),(x-2,y-1),(x-2,y+1)
, (x+1,y-2),(x+1,y+2),(x-1,y-2),(x-1,y+2)
]
knightMoves :: HasCheck m => Coord -> m (Set Coord)
knightMoves pos =
do let moveSet =
knightMoveSet pos
invalidMoves <- collisionKnightRule pos
return $ Set.difference moveSet invalidMoves
and an instance for the HasCheck class for an arbitrary coordinate
instance HasCheck Gen where
isCollision _ =
Quickcheck.arbitrary
and so afterwards to test this I want to ensure that the generated moveset is a proper subset of all possible moves.
knightSetProperty :: Piece.HasCheck Gen
=> (Int,Int)
-> Gen Bool
knightSetProperty position =
do moves <- Piece.knightMoves position
return $ moves `Set.isProperSubsetOf` (Piece.knightMoveSet position)
-- ... later on
it "Knight ruleset is subset" $
quickCheck knightSetProperty
Of course this fails because it could be that the knight can't move anywhere, which would mean that it's not a proper subset but the same set. However the error reported is not particularly helpful
*** Failed! Falsifiable (after 14 tests and 3 shrinks):
(0,0)
This is because quickcheck doesn't report the generated value of isCollision. Therefore I wonder, how can I make quickCheck report the generated value of isCollision
?
Okay so I feel this should be solvable in another way. However I made the following solution that works inspired by the handler pattern.
I changed the HasCheck typeclass to a record, as follows:
data Handle = MakeHandle
{ isCollision :: Coord -> Bool
}
and then refactored all the code to use handle instead of HasCheck.
collisionKnightRule :: Handle -> Coord -> (Set Coord)
collisionKnightRule handle =
Set.filter (isCollision handle) . knightMoveSet
-- | Set of all moves, legal or not
knightMoveSet :: Coord -> Set Coord
knightMoveSet (x,y) =
Set.fromList
[ (x+2,y-1),(x+2,y+1),(x-2,y-1),(x-2,y+1)
, (x+1,y-2),(x+1,y+2),(x-1,y-2),(x-1,y+2)
]
-- | Set of illegal moves
knightRuleSet :: Handle -> Coord -> (Set Coord)
knightRuleSet =
collisionKnightRule
knightMoves :: Handle -> Coord -> (Set Coord)
knightMoves handle pos =
let
moveSet =
knightMoveSet pos
invalidMoves =
knightRuleSet handle pos
in
Set.difference moveSet invalidMoves
The disadvantage of this is I fear that for stateful code it can be easy to introduce an error where you pass in a handle that is out-of-date, I.E. having multiple sources of truths. An advantage is that this is probably easier for people new to Haskell to understand. We can now mock functions using the Quickcheck's Function typeclass and pass them as an argument to make a mockHandler:
knightSetProperty ::
Fun (Int,Int) Bool
-> (Int,Int)
-> Gen Bool
knightSetProperty (Fun _ isCollision) position =
let
handler =
Piece.MakeHandle isCollision
moveSet =
Piece.knightMoves handler position
in
return $ moveSet `Set.isProperSubsetOf` (Piece.knightMoveSet position)
Now this fails properly with a counterexample:
*** Failed! Falsifiable (after 53 tests and 74 shrinks):
{_->False}
(0,0)