module Game.Mastermind (
Eval(Eval),
evaluate,
matching,
matchingSimple,
mixedRandomizedAttempt,
partitionSizes,
mainSimple,
mainRandom,
main,
propBestSeparatingCode,
) where
import qualified Game.Mastermind.CodeSet.Tree as CodeSetTree
import qualified Game.Mastermind.CodeSet as CodeSet
import Game.Mastermind.CodeSet
(intersection, (*&), (#*&), unit, empty, union, unions, cube, )
import Game.Utility (randomSelect, )
import qualified Data.NonEmpty.Set as NonEmptySet
import qualified Data.Map as Map
import qualified Data.Set as Set
import Data.NonEmpty ((!:))
import Data.List.HT (partition, )
import Data.Tuple.HT (mapPair, )
import Data.Maybe.HT (toMaybe, )
import Data.Maybe (listToMaybe, )
import Control.Monad (guard, when, replicateM, )
import qualified Control.Monad.Trans.State as MS
import qualified Control.Monad.Trans.Class as MT
import qualified System.Random as Rnd
import qualified System.IO as IO
data Eval = Eval Int Int
deriving (Eq, Ord, Show)
evaluate :: (Ord a) => [a] -> [a] -> Eval
evaluate code attempt =
uncurry Eval $
mapPair
(length,
sum . Map.elems .
uncurry (Map.intersectionWith min) .
mapPair (histogram,histogram) . unzip) $
partition (uncurry (==)) $
zip code attempt
histogram :: (Ord a) => [a] -> Map.Map a Int
histogram = Map.fromListWith (+) . map (\a -> (a,1))
selectFromHistogram :: (Ord a) => Map.Map a Int -> [(a, Map.Map a Int)]
selectFromHistogram hist =
map (\a -> (a, Map.update (\n -> toMaybe (n>1) (pred n)) a hist)) $
Map.keys hist
matchingSimple :: Ord a => Set.Set a -> [a] -> Int -> [[Set.Set a]]
matchingSimple alphabet code rightPlaces =
map
(zipWith
(\symbol right ->
if right
then Set.singleton symbol
else Set.delete symbol alphabet)
code) $
possibleRightPlaces (length code) rightPlaces
possibleRightPlaces :: Int -> Int -> [[Bool]]
possibleRightPlaces n rightPlaces =
if n < rightPlaces
then []
else
if n==0
then [[]]
else
(guard (rightPlaces>0) >>
(map (True:) $
possibleRightPlaces (n-1) (rightPlaces-1)))
++
(map (False:) $
possibleRightPlaces (n-1) rightPlaces)
matching :: (CodeSet.C set, Ord a) => Set.Set a -> [a] -> Eval -> set a
matching alphabet =
let findCodes =
foldr
(\(fixed,c) go rightSymbols floating0 ->
if fixed
then c #*& go rightSymbols floating0
else
(unions $ do
guard (rightSymbols > 0)
(src, floating1) <- selectFromHistogram floating0
guard (c /= src)
return $ src #*& go (rightSymbols-1) floating1)
`union`
(Set.difference
(Set.delete c alphabet)
(Map.keysSet floating0) *&
go rightSymbols floating0))
(\rightSymbols _floating ->
if rightSymbols>0
then empty
else unit)
in \code (Eval rightPlaces rightSymbols) ->
unions $
map
(\pattern ->
let patternCode = zip pattern code
in findCodes patternCode rightSymbols $
histogram $ map snd $ filter (not . fst) patternCode) $
possibleRightPlaces (length code) rightPlaces
partitionSizes :: (Ord a) => Set.Set a -> [a] -> [(Eval, Integer)]
partitionSizes alphabet code =
map (\eval -> (eval, CodeSetTree.size $ matching alphabet code eval)) $
possibleEvaluations (length code)
possibleEvaluations :: Int -> [Eval]
possibleEvaluations n = do
rightPlaces <- [0..n]
rightSymbols <- [0..n-rightPlaces]
return $ Eval rightPlaces rightSymbols
interaction ::
(CodeSetTree.T Char -> MS.StateT state Maybe [Char]) ->
state -> NonEmptySet.T Char -> Int -> IO ()
interaction select initial alphabet n =
let go state set =
case MS.runStateT (select set) state of
Nothing -> putStrLn "contradicting evaluations"
Just (attempt, newState) -> do
putStr $ show attempt ++ " " ++
show (CodeSet.size set, CodeSet.representationSize set,
Set.size (CodeSet.symbols set)) ++ " "
IO.hFlush IO.stdout
eval <- getLine
let evalHist = histogram eval
evalHistRem =
Map.keys $ Map.delete 'o' $ Map.delete 'x' evalHist
when (not $ null evalHistRem)
(putStrLn $ "ignoring: " ++ evalHistRem)
let rightPlaces = length (filter ('x' ==) eval)
rightSymbols = length (filter ('o' ==) eval)
if rightPlaces >= n
then putStrLn "I won!"
else go newState $ intersection set $
matching (NonEmptySet.flatten alphabet) attempt $
Eval rightPlaces rightSymbols
in go initial (cube alphabet n)
mainSimple :: NonEmptySet.T Char -> Int -> IO ()
mainSimple = interaction (MT.lift . listToMaybe . CodeSet.flatten) ()
minimax :: (Ord b) => (a -> [b]) -> [a] -> a
minimax _ [] = error "minimax of empty list"
minimax f (a0:rest) =
fst $
foldl
(\old@(_minA, minB) a ->
let (ltMinB, geMinB) = partition (<minB) $ f a
in if null geMinB then (a, maximum ltMinB) else old)
(a0, maximum $ f a0) rest
reduceAlphabet :: (CodeSet.C set, Ord a) => set a -> Set.Set a -> Set.Set a
reduceAlphabet set alphabet =
let symbols = CodeSet.symbols set
in Set.union symbols $ Set.fromList $ take 1 $ Set.toList $
Set.difference alphabet symbols
bestSeparatingCode ::
(CodeSet.C set, Ord a) => Int -> set a -> [[a]] -> [a]
bestSeparatingCode n set =
let alphabet = CodeSet.symbols set
in minimax $ \attempt ->
map (CodeSet.size . intersection set . matching alphabet attempt) $
possibleEvaluations n
bestSeparatingCodeHistogram ::
(CodeSet.C set, Ord a) => set a -> [[a]] -> [a]
bestSeparatingCodeHistogram set =
minimax $ \attempt ->
Map.elems $ histogram $ map (evaluate attempt) $ CodeSet.flatten set
propBestSeparatingCode ::
(CodeSet.C set, Ord a) => Int -> set a -> [[a]] -> Bool
propBestSeparatingCode n set attempts =
bestSeparatingCode n set attempts
==
bestSeparatingCodeHistogram set attempts
randomizedAttempt ::
(CodeSet.C set, Rnd.RandomGen g, Ord a) =>
Int -> set a -> MS.StateT g Maybe [a]
randomizedAttempt n set = do
randomAttempts <-
replicateM 10 $
replicateM n $
randomSelect . Set.toList $
CodeSet.symbols set
let possible = CodeSet.flatten set
somePossible =
let size = CodeSet.size set
num = 10
in map (CodeSet.select set) $
Set.toList $ Set.fromList $
take num $
map (flip div (fromIntegral num)) $
iterate (size+) 0
_ <- MT.lift $ listToMaybe possible
return $ bestSeparatingCode n set $ somePossible ++ randomAttempts
separatingRandomizedAttempt ::
(CodeSet.C set, Rnd.RandomGen g, Ord a) =>
Int -> Set.Set a -> set a -> MS.StateT g Maybe [a]
separatingRandomizedAttempt n alphabet0 set = do
case CodeSet.size set of
0 -> MT.lift Nothing
1 -> return $ head $ CodeSet.flatten set
2 -> return $ head $ CodeSet.flatten set
size ->
let alphabet = reduceAlphabet set alphabet0
alphabetSize = Set.size alphabet
bigSize = toInteger size
in if bigSize * (bigSize + toInteger alphabetSize ^ n) <= 1000000
then return $ bestSeparatingCodeHistogram set $
CodeSet.flatten set ++ replicateM n (Set.toList alphabet)
else randomizedAttempt n set
mixedRandomizedAttempt ::
(CodeSet.C set, Rnd.RandomGen g, Ord a) =>
Int -> set a -> MS.StateT g Maybe [a]
mixedRandomizedAttempt n set = do
case CodeSet.size set of
0 -> MT.lift Nothing
1 -> return $ head $ CodeSet.flatten set
2 -> return $ head $ CodeSet.flatten set
size ->
if size <= 100
then randomizedAttempt n set
else
fmap (CodeSet.select set) $
MS.state $ Rnd.randomR (0, size-1)
mainRandom :: NonEmptySet.T Char -> Int -> IO ()
mainRandom alphabet n = do
g <- Rnd.getStdGen
interaction
(separatingRandomizedAttempt n (NonEmptySet.flatten alphabet))
g alphabet n
main :: IO ()
main =
let alphabet = NonEmptySet.fromList ('a'!:['b'..'z'])
in if True
then mainRandom alphabet 5
else mainSimple alphabet 7