packages feed

halma-0.2.0.0: test/Game/Halma/Board/Tests.hs

{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}

module Game.Halma.Board.Tests (tests) where

import Test.HUnit hiding (Test)
import Test.QuickCheck hiding (Result)
import Test.Framework
import Test.Framework.Providers.HUnit (testCase)
import Test.Framework.Providers.QuickCheck2 (testProperty)

import Data.List (permutations, sortBy)
import Data.Maybe (isJust, fromJust)
import Data.Function (on)
import Control.Monad (forM_)
import qualified Data.Map.Strict as M

import Math.Geometry.Grid
import qualified Math.Geometry.Grid.HexagonalInternal as HI
import Game.Halma.Board

testRowsInDirection :: Assertion
testRowsInDirection =
  forM_ [minBound..maxBound] $ \halmaDir -> do
    let fieldsAtRow i = filter ((== i) . rowsInDirection halmaDir) (indices SmallGrid)
        expected = let xs = [0,1,2,3,4,13,12,11,10] in xs ++ [9] ++ reverse xs
    expected @=? map (length . fieldsAtRow) [-9..9]
    8    @=? rowsInDirection halmaDir (corner SmallGrid halmaDir)
    (-8) @=? rowsInDirection halmaDir (corner SmallGrid (oppositeDirection halmaDir))

-- | Cutoff equality comparison
(@=?*) :: (Eq a, Show a) => [a] -> [a] -> Assertion
expected @=?* actual = expected @=? take (length expected) actual

testDistancesFromCenter :: Assertion
testDistancesFromCenter = do
  [1,6,12,18,24,24,18,12,6,0,0,0] @=?* actual SmallGrid
  [1,6,12,18,24,30,30,24,18,12,6,0,0,0] @=?* actual LargeGrid
  where
    distCenter :: HalmaGrid s -> (Int, Int) -> Int
    distCenter hg = distance hg (0, 0)
    fieldsAtDistance :: HalmaGrid s -> Int -> [(Int, Int)]
    fieldsAtDistance hg d = filter ((== d) . distCenter hg) (indices hg)
    actual :: HalmaGrid s -> [Int]
    actual hg = map (length . fieldsAtDistance hg) [0,1..]

testBoundaryLength :: Assertion
testBoundaryLength = do
  48 @=? length (boundary SmallGrid)
  60 @=? length (boundary LargeGrid)

testDirectionTo :: Assertion
testDirectionTo = do
  let c = corner SmallGrid
  [HI.Northeast] @=? directionTo SmallGrid (c South) (c Southeast)
  [HI.Northeast] @=? directionTo SmallGrid (c South) (c Northeast)
  assertOneOf
    (directionTo SmallGrid (c South) (c North))
    (permutations [HI.Northeast, HI.Northwest])

assertOneOf :: (Eq a, Show a) => a -> [a] -> Assertion
assertOneOf actual validResults = assertBool msg (actual `elem` validResults)
  where msg = "Expected '" ++ show actual ++ "' to be one of '" ++ show validResults ++ "'"

numbersCorrect :: HalmaGrid size -> [Maybe Team] -> Bool
numbersCorrect halmaGrid = go 15 15 (numberOfFields halmaGrid - 2*15)
  where go :: Int -> Int -> Int -> [Maybe Team] -> Bool
        go 0 0 0 [] = True
        go !n !s !e (Just North : rs) = go (n-1) s e rs
        go !n !s !e (Just South : rs) = go n (s-1) e rs
        go !n !s !e (Nothing : rs) = go n s (e-1) rs
        go _ _ _ _ = False

testInitialBoard :: Assertion
testInitialBoard = ass SmallGrid >> ass LargeGrid
  where
    ass hg = assertBool "Expected 15 pieces of team north and south" $ numbersCorrect hg (pieces hg)
    pieces hg = map (\p -> lookupHalmaBoard p (initialBoard hg twoPlayers)) (indices hg)
    twoPlayers :: Team -> Bool
    twoPlayers North = True
    twoPlayers South = True
    twoPlayers _ = False

arbitraryPerm :: [a] -> Gen [a]
arbitraryPerm xs =
  fmap (map fst . sortBy (compare `on` snd) . zip xs)
       (vectorOf (length xs) arbitrary :: Gen [Double])

genBoard :: HalmaGrid size -> Gen (HalmaBoard size)
genBoard halmaGrid = do
  pieces <- arbitraryPerm (indices halmaGrid)
  let (northTeam, restPieces) = splitAt 15 pieces
      southTeam = take 15 restPieces
  return $ fromJust $ fromMap halmaGrid $ M.fromList $
    map (flip (,) North) northTeam ++
    map (flip (,) South) southTeam

instance Arbitrary (HalmaBoard 'S) where
  arbitrary = genBoard SmallGrid

instance Arbitrary (HalmaBoard 'L) where
  arbitrary = genBoard LargeGrid

genHalmaGridPos :: HalmaGrid size -> Gen (Int, Int)
genHalmaGridPos halmaGrid = elements (indices halmaGrid)

prop_fromMap :: HalmaBoard size -> Bool
prop_fromMap halmaBoard = fromMap (getGrid halmaBoard) (toMap halmaBoard) == Just halmaBoard

prop_moveNumbersInvariant :: HalmaBoard size -> Gen Bool
prop_moveNumbersInvariant halmaBoard = do
  let halmaGrid = getGrid halmaBoard
  startPos <- genHalmaGridPos halmaGrid
  endPos <- genHalmaGridPos halmaGrid
  case movePiece startPos endPos halmaBoard of
    Left _err ->
      -- may only fail when there is no piece on the start position or a piece
      -- on the end position
      return $ lookupHalmaBoard startPos halmaBoard == Nothing || isJust (lookupHalmaBoard endPos halmaBoard)
    Right halmaBoard' -> do
      let pieces = map (\p -> lookupHalmaBoard p halmaBoard') (indices halmaGrid)
      return $ lookupHalmaBoard endPos halmaBoard' == lookupHalmaBoard startPos halmaBoard &&
               lookupHalmaBoard endPos halmaBoard  == lookupHalmaBoard startPos halmaBoard' &&
               numbersCorrect halmaGrid pieces


tests :: Test
tests = testGroup "Game.Halma.Board.Tests"
  [ testCase "testRowsInDirection" testRowsInDirection
  , testCase "testDistancesFromCenter" testDistancesFromCenter
  , testCase "testBoundaryLength" testBoundaryLength
  , testCase "testDirectionTo" testDirectionTo
  , testCase "testInitialBoard" testInitialBoard
  , testProperty "prop_fromMap(S)" (prop_fromMap :: HalmaBoard 'S -> Bool)
  , testProperty "prop_fromMap(L)" (prop_fromMap :: HalmaBoard 'L -> Bool)
  , testProperty "prop_moveNumbersInvariant(S)" (prop_moveNumbersInvariant :: HalmaBoard 'S -> Gen Bool)
  , testProperty "prop_moveNumbersInvariant(L)" (prop_moveNumbersInvariant :: HalmaBoard 'L -> Gen Bool)
  ]