packages feed

hstzaar-0.5: src/Tests.hs

{-
  Quickcheck properties for board & AI code
  Pedro Vasconcelos, 2010
-}
module Tests (run_tests) where
import Board 
import AI.Minimax
import AI.Utils
import AI.Eval
import Test.QuickCheck
import qualified Data.IntMap as IntMap
import qualified Data.IntSet as IntSet
import List (delete, nub, sort)


---------------------------------------------------------------------------
-- Quickcheck properties 
---------------------------------------------------------------------------

-- a capture reduces the number of pieces by one
prop_capture_moves :: Board -> Bool
prop_capture_moves b
    = and [1+boardSize b' == boardSize b |
           m<-nextCaptureMoves b, let b' = applyMove b m]

-- a stacking reduces the number of pieces by one
prop_stacking_moves1 :: Board -> Bool
prop_stacking_moves1 b
    = and [1+boardSize b' == boardSize b |
           m<-nextStackingMoves b, let b' = applyMove b m]

-- stacking mantains the sum of pieces heights of the active player
-- and does not change the pieces of the other player
prop_stacking_moves2 :: Board -> Bool
prop_stacking_moves2 b
    = and [ heights (active b') == heights (active b) && inactive b' == inactive b | 
             m <- nextStackingMoves b, let b'=applyMove b m]
    where heights b = sum [h | (_,h)<-IntMap.elems b]


---------------------------------------------------------------------------
-- some properties of the AI code
---------------------------------------------------------------------------

{-
-- static evaluation respects the zero-sum property
prop_zero_sum :: Board -> Property
prop_zero_sum b
    = admissible b ==> eval b - eval (swapBoard b) == 0
-}

-- upper and lower bounds for the evaluation function
prop_value_bounds :: Board -> Property
prop_value_bounds b
    = not (active_lost b) && not (inactive_lost b) ==> abs value < infinity
    where value = static_eval b


-- end game positions give plus/minus infinityinity scores
prop_inactive_lost :: Board -> Property
prop_inactive_lost b
    = not (active_lost b) && inactive_lost b ==> 
      static_eval b == infinity 

prop_active_lost :: Board -> Property
prop_active_lost b
    = not (inactive_lost b) && active_lost b ==> 
      static_eval b == (-infinity)



-- alpha-beta pruning computes the minimax value
-- parameters: number of pieces, pruning depth and breadth
prop_alpha_beta :: Int -> Int -> Int -> Property
prop_alpha_beta npieces depth breadth
    = forAllShrink (resize npieces arbitrary) shrink $ \b ->
      not (active_lost b) ==>
          let bt = mkTree depth breadth b
          in minimax_ab (-infinity) infinity bt == minimax bt
    

-- extended alpha-beta minimax computes the first move of the principal variation
-- parameters: number of pieces, pruning depth and breadth
prop_alpha_beta_move :: Int -> Int -> Int -> Property
prop_alpha_beta_move npieces depth breadth
    = forAllShrink (resize npieces arbitrary) shrink $ \b ->
      not (active_lost b)  ==> 
          let bt = mkTree depth breadth b
              (m,v)= minimaxMove_ab (-infinity) infinity bt
              bt' = treeMove m bt
          in  minimax bt' == -v


mkTree :: Int -> Int -> Board -> GameTree Int Turn
mkTree depth breadth board = pruneDepth depth $ 
                             pruneBreadth breadth $ 
                             lowFirst $
                             mapTree static_eval $ 
                             boardTree board


treeMove :: Eq m => m -> GameTree s m -> GameTree s m
treeMove m (GameTree _ branches) = head [t | (m',t)<-branches, m'==m]


-- 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 = nextCaptureMoves b
      moves2 = concat [nextCaptureMoves (applyMove b m) | m<-moves1]
      pos = IntSet.fromList (map snd moves1 ++ map snd moves2)
      pos'= IntMap.keysSet (zoneOfControl b)


-- 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 = null (nextCaptureMoves b) || pieceTypes (active b)/= 3
inactive_lost = active_lost . swapBoard


-- 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_zero_sum", quickCheck prop_zero_sum)
            , ("prop_value_bounds", quickCheck prop_value_bounds)
            , ("prop_inactive_lost", quickCheck prop_inactive_lost)
            , ("prop_active_lost", quickCheck prop_active_lost)
            , ("prop_zoc_correct", quickCheck prop_zoc_correct)
            --, ("prop_zoc_correct2", quickCheck prop_zoc_correct2)
            , ("prop_alpha_beta 10 4 5",
               quickCheck (prop_alpha_beta 10 4 5))
            , ("prop_alpha_beta 15 6 5",
               quickCheck (prop_alpha_beta 15 6 5))
            , ("prop_alpha_beta_move 10 4 5",
               quickCheck (prop_alpha_beta_move 10 4 5))
            , ("prop_alpha_beta_move 15 6 5",
               quickCheck (prop_alpha_beta_move 15 6 5))
            ]



quickCheckN n = quickCheckWith (stdArgs{maxSuccess=n})