board-games-0.3: test/Test/Mastermind.hs
module Test.Mastermind (tests) where
import qualified Game.Mastermind.CodeSet.Tree as CodeSetTree
-- import qualified Game.Mastermind.CodeSet.Union as CodeSetUnion
import qualified Game.Mastermind.CodeSet as CodeSet
import qualified Game.Mastermind.NonEmptyEnumSet as NonEmptySet
import qualified Game.Mastermind as MM
import Game.Utility (Choice, mergeChoice, noChoice)
import Control.Monad (liftM2, )
import Control.Applicative ((<$>), )
import qualified Data.NonEmpty.Class as NonEmptyC
import qualified Data.NonEmpty as NonEmpty
import qualified Data.Traversable as Trav
import qualified Data.EnumSet as EnumSet
import Data.EnumSet (EnumSet)
import Data.NonEmpty ((!:))
import qualified Test.QuickCheck as QC
import Test.QuickCheck (Property, Arbitrary(arbitrary), quickCheck, (==>), )
alphabet :: EnumSet Int
alphabet = NonEmptySet.flatten neAlphabet
neAlphabet :: NonEmptySet.T Int
neAlphabet = NonEmptySet.fromList $ 0!:[1..9]
newtype Code = Code [Int]
deriving (Show)
genElement :: QC.Gen Int
genElement = QC.choose (0,9)
-- can we get it working with empty lists, too?
genCode :: Int -> QC.Gen Code
genCode width =
fmap (Code . take width) $ QC.listOf1 genElement
-- fmap (Code . take width) (QC.listOf genElement)
instance Arbitrary Code where
arbitrary = genCode 5
data CodePair = CodePair [Int] [Int]
deriving (Show)
genCodePair :: Int -> QC.Gen CodePair
genCodePair width =
liftM2
(\(Code xs) (Code ys) ->
uncurry CodePair $ unzip $ zip xs ys)
(genCode width) (genCode width)
instance Arbitrary CodePair where
arbitrary = genCodePair 5
matchingMember :: CodePair -> Bool
matchingMember (CodePair secret attempt) =
CodeSetTree.member secret $
MM.matching alphabet attempt (MM.evaluate secret attempt)
genEval :: Int -> QC.Gen MM.Eval
genEval size = do
total <- QC.frequency $ map (\k -> (k+1, return k)) [1 .. size]
rightPlaces <- QC.choose (0,total)
return $ MM.Eval rightPlaces (total - rightPlaces)
forAllEval :: QC.Testable prop => [a] -> (MM.Eval -> prop) -> Property
forAllEval code = QC.forAll (genEval (length code))
matchingNotMember :: CodePair -> Property
matchingNotMember (CodePair secret attempt) =
forAllEval secret $ \eval ->
(eval == MM.evaluate secret attempt)
==
(CodeSetTree.member secret $ MM.matching alphabet attempt eval)
matchingDisjoint :: Code -> Property
matchingDisjoint (Code attempt) =
forAllEval attempt $ \eval0 ->
forAllEval attempt $ \eval1 ->
let matching0 = MM.matching alphabet attempt eval0
matching1 = MM.matching alphabet attempt eval1
in eval0 == eval1 ||
CodeSetTree.null (CodeSetTree.intersection matching0 matching1)
evaluateCommutative :: CodePair -> Bool
evaluateCommutative (CodePair secret attempt) =
MM.evaluate secret attempt
==
MM.evaluate attempt secret
type CodeSetInt = CodeSetTree.T Int
evaluateMatching :: Code -> Property
evaluateMatching (Code attempt) =
forAllEval attempt $ \eval ->
all ((eval ==) . MM.evaluate attempt) $
take 100 $
CodeSet.flatten $
(MM.matching alphabet attempt eval :: CodeSetInt)
{-
A more precise test would be to check
that for different numbers of rightPlace and rightSymbol
the codesets are disjoint
and their union is the set of all possible codes.
To this end we need a union with simplification or a subset test.
-}
partitionSizes :: Code -> Bool
partitionSizes (Code attempt) =
fromIntegral (EnumSet.size alphabet) ^ length attempt
==
sum (map snd (MM.partitionSizes alphabet attempt))
selectFlatten :: Code -> Property
selectFlatten (Code attempt) =
forAllEval attempt $ \eval ->
let set :: CodeSetInt
set = MM.matching alphabet attempt eval
in map (CodeSet.select set) [0 .. min 100 (CodeSet.size set) - 1]
==
take 100 (CodeSet.flatten set)
genFixedLengthCodes :: (NonEmptyC.Gen f) => Int -> QC.Gen (f [Int])
genFixedLengthCodes width = NonEmptyC.genOf $ QC.vectorOf width genElement
bestSeparatingCode :: Property
bestSeparatingCode =
QC.forAll (genCodePair 4) $ \(CodePair base0 base1) ->
forAllEval base0 $ \eval0 ->
forAllEval base1 $ \eval1 -> do
let width = length base0
set =
CodeSet.intersection
(MM.matching alphabet base0 eval0)
(MM.matching alphabet base1 eval1)
not (CodeSet.null set) ==>
QC.forAll (fmap (NonEmpty.mapTail $ take 9) $ genFixedLengthCodes width) $
MM.propBestSeparatingCode width (set :: CodeSetInt)
intersections :: Property
intersections =
QC.forAll (genCode 4) $ \(Code code) ->
QC.forAll (fmap (take 10) $ genFixedLengthCodes (length code)) $ \codes ->
QC.forAll (Trav.mapM (\x -> (,) x <$> genEval (length code)) (code!:codes)) $
CodeSetTree.propIntersections . fmap (uncurry $ MM.matching alphabet)
-- should also work, when selecting any code from the set of matching codes
solve :: Code -> Bool
solve (Code secret) =
let recourse remain =
case CodeSet.flatten remain of
[] -> False
[attempt] -> secret == attempt
attempt:_ ->
recourse $ CodeSet.intersection remain $
MM.matching alphabet attempt $ MM.evaluate secret attempt
in recourse (CodeSet.cube neAlphabet (length secret) :: CodeSetInt)
{-
Other possible tests:
the products in a set produced by 'MM.matching' must be disjoint.
set laws for the two set implementations,
such as distributivity of union and intersection
check member against intersection with singleton
-}
choiceLeftIdentity :: Choice Char -> Bool
choiceLeftIdentity a =
a == mergeChoice noChoice a
choiceRightIdentity :: Choice Char -> Bool
choiceRightIdentity a =
a == mergeChoice a noChoice
choiceCommutative :: Choice Char -> Choice Char -> Bool
choiceCommutative a b =
mergeChoice a b == mergeChoice b a
{-
Unfortunately, this does not apply:
*Test.Mastermind EnumMap> let a = Choice (EnumMap.singleton 'x' 1) 1
*Test.Mastermind EnumMap> let b = Choice (EnumMap.singleton 'x' 1) 0
*Test.Mastermind EnumMap> let c = Choice (EnumMap.singleton 'y' 1) 1
*Test.Mastermind EnumMap> mergeChoice (mergeChoice a b) c
Choice (fromList [('x',1),('y',1)]) 2
*Test.Mastermind EnumMap> mergeChoice a (mergeChoice b c)
Choice (fromList [('x',1),('y',1)]) 1
*Test.Mastermind EnumMap> mergeChoice a b
Choice (fromList [('x',1)]) 1
*Test.Mastermind EnumMap> mergeChoice b c
Choice (fromList [('x',1),('y',1)]) 1
-}
_choiceAssociative :: Choice Char -> Choice Char -> Choice Char -> Bool
_choiceAssociative a b c =
mergeChoice (mergeChoice a b) c
==
mergeChoice a (mergeChoice b c)
tests :: [(String, IO ())]
tests =
("matchingMember", quickCheck matchingMember) :
("matchingNotMember", quickCheck matchingNotMember) :
("matchingDisjoint", quickCheck matchingDisjoint) :
("evaluateCommutative", quickCheck evaluateCommutative) :
("evaluateMatching", quickCheck evaluateMatching) :
("partitionSizes", quickCheck partitionSizes) :
("selectFlatten", quickCheck selectFlatten) :
("bestSeparatingCode", quickCheck bestSeparatingCode) :
("intersections", quickCheck intersections) :
("solve", quickCheck solve) :
("choiceLeftIdentity", quickCheck choiceLeftIdentity) :
("choiceRightIdentity", quickCheck choiceRightIdentity) :
("choiceCommutative", quickCheck choiceCommutative) :
-- ("choiceAssociative", quickCheck choiceAssociative) :
[]