set-cover-0.0.5: example/Mastermind.hs
{- |
https://en.wikipedia.org/wiki/Mastermind_(board_game)
Given a list of guesses and according evaluations,
the solver computes a list of all possible codes
that match the obtained evaluations.
See also the @board-games@ package.
-}
module Main where
import qualified Math.SetCover.Exact as ESC
import qualified System.IO as IO
import System.Random (StdGen, getStdGen, randomR, )
import qualified Control.Monad.Trans.State as MS
import Control.Monad (liftM2, replicateM, when, )
import qualified Data.Set as Set; import Data.Set (Set, )
import qualified Data.Array as Array
import qualified Data.List.Match as Match
import qualified Data.List.HT as ListHT
import Data.Tuple.HT (mapSnd, )
import Data.List.HT (tails, viewL, viewR, )
import Data.Maybe (mapMaybe, )
-- cf. htam:Combinatorics.tuples
choose :: Int -> [a] -> [[a]]
choose n xs =
flip MS.evalStateT xs $ replicateM n $
MS.StateT $ mapMaybe viewL . tails
data X = Pos Int | Eval Eval Int Int | EvalRow Eval Int
deriving (Eq, Ord, Show)
data Eval = CorrectPlace | CorrectSymbol
deriving (Eq, Ord, Show)
type Assign a = ESC.Assign [(Int, a)] (Set X)
assignsFromGuesses ::
(Ord a) =>
Int -> [a] -> [([a], (Int,Int))] -> [Assign a]
assignsFromGuesses width set guesses =
liftM2
(\pat a ->
let ks = map fst $ filter snd $ zip [0..] pat
in ESC.assign (map (flip (,) a) ks) $ Set.unions $
Set.fromList (map Pos ks) :
zipWith
(\row (guess,_) ->
Set.fromList $
let (correctlyPlaced, remGuess) =
ListHT.partition (\(_k, (used,equ)) -> used && equ) $
zip [0..] $ zip pat $ map (a==) guess
in map (Eval CorrectPlace row . fst) correctlyPlaced
++
map (Eval CorrectSymbol row . fst)
(Match.take
(filter (fst . snd) remGuess)
(filter (snd . snd) remGuess)))
[0..] guesses)
(tail $ replicateM width [False, True]) set
++
concat
(zipWith
(\row (_, (correctPlaces,correctSymbols)) ->
let fill eval k =
map (ESC.assign [] . Set.fromList . (EvalRow eval row :)) $
choose (width - k) $
map (Eval eval row) $ take width [0..]
in fill CorrectPlace correctPlaces
++
fill CorrectSymbol correctSymbols)
[0..] guesses)
codeFromLabels :: [[(Int, a)]] -> [a]
codeFromLabels mxs =
case concat mxs of
xs -> Array.elems $ Array.array (0, length xs - 1) xs
unique :: (Ord a) => [a] -> Bool
unique xs = Set.size (Set.fromList xs) == length xs
newGuess ::
(Ord a) =>
Int -> [a] -> [([a], (Int,Int))] -> MS.State StdGen (Maybe [a])
newGuess width alphabet oldGuesses = do
n <- MS.state $ randomR (1,1000)
return $ fmap snd $ viewR $ take n $
-- filter unique $
map codeFromLabels $ ESC.partitions $
assignsFromGuesses width alphabet oldGuesses
countEval :: String -> ((Int, Int), String)
countEval eval0 =
let (correctPlaces, eval1) = ListHT.partition ('x' ==) eval0
(correctSymbols, eval2) = ListHT.partition ('o' ==) eval1
in ((length correctPlaces, length correctSymbols), eval2)
{- |
In every round the computer player selects randomly one of the first 1000 codes
that are coherent with the known evaluations.
This strategy prevents stupid guesses like "aaaaa",
but it does not minimize the number of guesses.
When the game approaches the end
there is often only one unknown letter left
and the algorithm makes a guess for ruling out every single candidate.
It would be more efficient to use non-coherent guesses in this situation
in order to rule out a whole bunch of candidates at once.
-}
interaction :: Int -> [Char] -> IO ()
interaction width alphabet =
let go guesses g0 =
case MS.runState (newGuess width alphabet guesses) g0 of
(Nothing, _) -> putStrLn "contradicting evaluations"
(Just attempt, g1) -> do
putStr $ show attempt ++ " "
IO.hFlush IO.stdout
eval0 <- getLine
let ((numPlaces, numSymbols), evalRem) = countEval eval0
when (not $ null evalRem) (putStrLn $ "ignoring: " ++ evalRem)
if numPlaces >= width
then putStrLn "Code found!"
else go ((attempt, (numPlaces, numSymbols)) : guesses) g1
in go [] =<< getStdGen
testGuesses :: [(String, (Int, Int))]
testGuesses =
map (mapSnd (fst . countEval)) $
("aaaayw", "x") :
("bbbdcw", "") :
("eefeym", "oo") :
("iuzamf", "oo") :
("gvarfe", "ooo") :
("paqfes", "xxo") :
("vamsej", "ooxx") :
("amgses", "ooox") :
("majgep", "xxx") :
[]
testSolve :: IO ()
testSolve =
mapM_ (print . codeFromLabels) $ ESC.partitions $
assignsFromGuesses 6 ['a'..'z'] testGuesses
main :: IO ()
main = do
let n = 5
putStrLn $
"Come up with a word consisting of " ++ show n ++
" letters and evaluate my guesses."
putStrLn "Enter 'x's for correct places and 'o's for correct symbols in any order."
interaction n ['a'..'z']