hstzaar-0.9.4: src/Tests.hs
{-
Quickcheck properties for board & AI code
Pedro Vasconcelos, 2010, 2011
-}
module Tests where
import Board
--import AI.Tree
import AI.Eval
import AI.Minimax
import qualified Data.Map as Map
import qualified Data.HashMap.Strict as HashMap
import qualified Data.Set as Set
import qualified Data.Vector.Unboxed as Vec
import Data.List (delete, nub, sort)
import Control.Monad (liftM)
import Test.QuickCheck
------------------------------------------------------------------------
-- | QuickCheck generators
------------------------------------------------------------------------
-- generators for board elements
instance Arbitrary Color where
arbitrary = elements [White, Black]
instance Arbitrary Kind where
arbitrary = elements [Tzaar,Tzarra,Tott]
-- default generator and counter-example shrinker for boards
instance Arbitrary Board where
arbitrary = sized genBoard
{-
shrink board
= [board {active=you} | you<-shrinkHalf (active board)] ++
[board {inactive=other} | other<-shrinkHalf (inactive board)]
-- helper function to shrink half-boards
-- first try to remove pieces, then reduce heights
shrinkHalf :: HalfBoard -> [HalfBoard]
shrinkHalf b = [Map.delete p b | p<-Map.keys b] ++
[Map.insert p (t,h') b |
(p,(t,h))<-Map.assocs b, h'<-[1..h-1]]
-}
-- generator for boards
-- size argument is a bound for the total number of pieces
-- always generates board with the 3 kinds for each player
genBoard :: Int -> Gen Board
genBoard size
= do ws <- genStacks White n
bs <- genStacks Black n
ps <- genShuffle positions
c <- arbitrary
--m <- choose (1, n)
let whites = zip (take n ps) ws
let blacks = zip (drop n ps) bs
let pmap = HashMap.fromList (whites++blacks)
return Board { active = c
, move = 0
-- , moves = []
, pieces = pmap
, activeCounts = Vec.fromList $ countStacks c pmap
, inactiveCounts = Vec.fromList $ countStacks (invert c) pmap
, activeHeights = Vec.fromList $ sumHeights c pmap
, inactiveHeights= Vec.fromList $ sumHeights (invert c) pmap
}
where n = 3 `max` (size`div`2) `min` 30 -- between 3 and 30 stacks
-- generate piece stacks
genStacks :: Color -> Int -> Gen [Piece]
genStacks c n
= do ps <- liftM ([Tzaar,Tzarra,Tott]++) (genShuffle pieces)
hs <- sequence [choose (1,maxHeight) | _<-[1..n]]
return (zipWith3 Piece (repeat c) ps hs)
where pieces = replicate 5 Tzaar ++
replicate 8 Tzarra ++
replicate 14 Tott
maxHeight = 5
-- generate random permutations of a list
genShuffle :: Eq a => [a] -> Gen [a]
genShuffle [] = return []
genShuffle xs = do x <- elements xs
xs'<- genShuffle (delete x xs)
return (x:xs')
---------------------------------------------------------------------------
-- Quickcheck properties
---------------------------------------------------------------------------
-- properties of the game mechanics
-- a capture reduces the number of pieces by one
prop_capture_moves :: Board -> Bool
prop_capture_moves b
= and [countPieces b == 1+countPieces b' |
m <- captureMoves b, let b' = applyMove m b]
-- a stacking reduces the number of pieces by one
prop_stacking_moves1 :: Board -> Bool
prop_stacking_moves1 b
= and [countPieces b == 1+ countPieces b' |
m <- stackingMoves b, let b' = applyMove m b]
-- stacking mantains the sum of pieces heights of the active player
prop_stacking_moves2 :: Board -> Bool
prop_stacking_moves2 b
= and [ heights (pieces b) == heights (pieces b')
| m <- stackingMoves b, let b'=applyMove m b]
where
c = active b -- the current player
heights ps = sum [h | (Piece c' _ h)<-HashMap.elems ps, c'==c]
-- stacking does not modify opponents pieces
prop_stacking_moves3 :: Board -> Bool
prop_stacking_moves3 b
= and [ HashMap.filter (\p->color p==c') (pieces b') ==
HashMap.filter (\p->color p==c') (pieces b)
| m <- stackingMoves b, let b'=applyMove m b]
where
c = active b -- the current player
c'= invert c -- the other player
prop_swap_swap :: Board -> Bool
prop_swap_swap b = swapPlayer (swapPlayer b) == b
prop_active_counts :: Board -> Bool
prop_active_counts b
= and [activeCounts b' == Vec.fromList (countStacks (active b') (pieces b'))
| m<-nextMoves b, let b'=applyMove m b]
prop_inactive_counts :: Board -> Bool
prop_inactive_counts b
= and [inactiveCounts b' == Vec.fromList (countStacks (inactive b') (pieces b'))
| m<-nextMoves b, let b'=applyMove m b]
prop_active_heights :: Board -> Bool
prop_active_heights b
= and [activeHeights b' == Vec.fromList (sumHeights (active b') (pieces b'))
| m<-nextMoves b, let b'=applyMove m b]
prop_inactive_heights :: Board -> Bool
prop_inactive_heights b
= and [inactiveHeights b' == Vec.fromList (sumHeights (inactive b') (pieces b'))
| m<-nextMoves b, let b'=applyMove m b]
-- correctness of the zone of control computation
-- the zone of control is the set of pieces
-- that can be captured in a turn (one or two moves)
prop_zoc_correct :: Board -> Bool
prop_zoc_correct b = pos == pos'
where
moves1 = captureMoves b
moves2 = concat [captureMoves (swapPlayer (applyMove m b)) | m<-moves1]
pos = Set.fromList [dest | Capture _ dest<-(moves1++moves2)]
pos'= Set.fromList $ HashMap.keys $ zoneOfControl (active b) (pieces b)
---------------------------------------------------------------------------
-- properties of the AI code
---------------------------------------------------------------------------
-- | a trace is a sequence of game positions
newtype Trace = Trace { unTrace :: [Board] } deriving Show
instance Arbitrary Trace where
arbitrary = do b <- arbitrary
liftM Trace (genTrace b)
-- | generate a sequence random board resulting
-- from valid moves from a starting board
genTrace :: Board -> Gen [Board]
genTrace b
| null ms = return [b]
| otherwise = do m <- elements ms
bs <- genTrace (applyMove m b)
return (b:bs)
where ms = nextMoves b
-- | players must alternate in a trace
prop_trace_alternating (Trace bs)
= let players = map active bs
in and $ zipWith (/=) players (tail players)
-- | both players have 3 kinds of pieces until the end of the game
prop_trace_ending (Trace bs)
= let b' = last bs
bs'= init bs
in all threekinds bs' &&
(lostone b' || null (nextMoves b'))
where threekinds b = all (>0) (countStacks (active b) (pieces b) ++
countStacks (inactive b) (pieces b))
lostone b = any (==0) (countStacks (active b) (pieces b))
-- | upper and lower bounds for the evaluation function
--prop_value_bounds :: StaticEval -> Trace -> Bool
--prop_value_bounds f (Trace bs)
-- = let vs = map f bs in all (\v -> abs v<=infinity) vs
{-
-- correcteness of alpha-beta pruning against plain minimax
-- parameters: number of pieces, pruning depth
prop_alpha_beta :: Int -> Board -> Bool
prop_alpha_beta d b
= let bt = pruneDepth d $ mapTree eval1 $ boardTree b
in negamax_alpha_beta (minBound+1) maxBound bt == negamax bt
-- correctness of alpha-beta minimax extended with principal variation
-- parameters: number of pieces, pruning depth
prop_alpha_beta_pv :: Int -> Board -> Bool
prop_alpha_beta_pv d b
= let bt = pruneDepth d $ mapTree eval1 $ boardTree b
PV v ms = undefined -- negamaxPV bt
n = length ms
b' = foldl (flip applyMove) b ms
v' = eval1 b'
in (-1)^n * v' == v
-}
{-
-- end game positions give plus/minus infinity scores
prop_inactive_lost :: Eval -> Board -> Property
prop_inactive_lost f b
= not (active_lost b) && inactive_lost b ==> f b == infinity
prop_active_lost :: Eval -> Board -> Property
prop_active_lost f b
= not (inactive_lost b) && active_lost b ==> f b == (-infinity)
treeMove :: Eq m => GameTree s m -> m -> GameTree s m
treeMove (GameTree _ branches) m = head [t | (m',t)<-branches, m'==m]
mkTree :: Int -> Eval -> Board -> GameTree Int Move
mkTree depth eval board = pruneDepth depth $ mapTree eval $ boardTree board
wellformedTree :: GameTree Board Move -> Bool
wellformedTree (GameTree b branches)
= and [player b /= player b' &&
wellformedTree t | (m,t@(GameTree b' _)) <- branches]
-- helper functions to filter boards, etc.
-- "admissible" boards: no winner yet
admissible :: Board -> Bool
admissible b = not (active_lost b) && not (inactive_lost b)
active_lost, inactive_lost :: Board -> Bool
active_lost b
= (move b==1 && null (captureMoves b)) ||
any (==0) (countStacks $ active b)
inactive_lost b = any (==0) (countStacks $ inactive b)
-- number of piece types in a half-board
--pieceTypes :: HalfBoard -> Int
--pieceTypes b = length $ nub $ map fst $ IntMap.elems b
-}
-- run all tests
run_tests :: IO ()
run_tests = mapM_ run_test all_tests
where run_test (name, test) = putStrLn (">>> " ++ name) >> test
all_tests = [ ("prop_capture_moves", quickCheck prop_capture_moves)
, ("prop_stacking_moves1", quickCheck prop_stacking_moves1)
, ("prop_stacking_moves2", quickCheck prop_stacking_moves2)
, ("prop_stacking_moves3", quickCheck prop_stacking_moves3)
, ("prop_swap_swap", quickCheck prop_swap_swap)
, ("prop_active_counts", quickCheck prop_active_counts)
, ("prop_inactive_counts", quickCheck prop_inactive_counts)
, ("prop_active_heights", quickCheck prop_active_heights)
, ("prop_inactive_heights", quickCheck prop_inactive_heights)
--, ("prop_move_count", quickCheck prop_move_count)
, ("prop_zoc_correct", quickCheck prop_zoc_correct)
, ("prop_trace_alternating", quickCheck prop_trace_alternating)
, ("prop_trace_ending", quickCheck prop_trace_ending)
--, ("prop_value_bounds", quickCheck (prop_value_bounds eval1))
--, ("prop_alpha_beta_pv 3", quickCheck (prop_alpha_beta_pv 3))
--, ("prop_alpha_beta_pv 5", quickCheck (prop_alpha_beta_pv 5))
--, ("prop_alpha_beta_pv 6", quickCheck (prop_alpha_beta_pv 6))
]
quickCheckN n = quickCheckWith (stdArgs{maxSuccess=n})