packages feed

board-games-0.3: benchmark/MastermindStrategy.hs

module Main where

import qualified Game.Mastermind as MM
import qualified Game.Mastermind.CodeSet.Tree as CodeSetTree
import qualified Game.Mastermind.CodeSet as CodeSet
import qualified Game.Mastermind.NonEmptyEnumSet as NonEmptySet
import Game.Utility (randomSelect, histogram)

import qualified System.Random as Rnd
import qualified System.IO as IO

import qualified Control.Parallel.Strategies as Strategy

import qualified Control.Monad.Trans.State as MS
import qualified Control.Functor.HT as FuncHT
import Control.Monad (replicateM, liftM2)

import Text.Printf (printf)

import qualified Data.Traversable as Trav
import qualified Data.Foldable as Fold
import qualified Data.NonEmpty as NonEmpty
import qualified Data.Empty as Empty
import qualified Data.List as List
import qualified Data.Map as Map
import qualified Data.Zip as Zip
import Data.NonEmpty ((!:))
import Data.Maybe (fromMaybe)


justState :: MS.State g (Maybe a) -> MS.State g a
justState = fmap $ fromMaybe (error "contradicting evaluation")

play ::
   (Rnd.RandomGen g) =>
   (Int -> [([Char], MM.Eval)] ->
    CodeSetTree.T Char -> MS.State g (Maybe String)) ->
   NonEmptySet.T Char -> String -> MS.State g [String]
play doGuess alphabet code = do
   let width = length code
   let go oldGuesses codeSet = do
         guess <- justState $ doGuess width oldGuesses codeSet
         let eval = MM.evaluate guess code
         let currentGuesses = oldGuesses++[(guess,eval)]
         if eval == MM.Eval width 0
            then return $ fmap fst currentGuesses
            else go currentGuesses $
                  CodeSetTree.intersection codeSet $
                  MM.matching (NonEmptySet.flatten alphabet) guess eval
   go [] $ CodeSet.cube alphabet width


type Tuple = NonEmpty.T (NonEmpty.T (NonEmpty.T (NonEmpty.T Empty.T)))

playVariants ::
   (Rnd.RandomGen g) =>
   NonEmptySet.T Char -> String -> MS.State g (String, Tuple Int)
playVariants alphabet code = do
   let alphabetFlat = NonEmptySet.flatten alphabet
   guessesColumns <-
      Trav.mapM (\strategy -> play strategy alphabet code) $
         (\width _oldGuesses -> MM.mixedRandomizedAttempt width) !:
         (\width _oldGuesses -> MM.randomizedAttempt width) !:
         (\width oldGuesses ->
            MM.scanningRandomizedAttempt width alphabetFlat oldGuesses) !:
         (\width _oldGuesses ->
            MM.separatingRandomizedAttempt width alphabetFlat) !:
         Empty.Cons
   return (code, fmap length guessesColumns)

playMany ::
   (Rnd.RandomGen g) =>
   NonEmpty.T [] Char -> Int -> g -> [(String, Tuple Int)]
playMany symbols width =
   let alphabet = NonEmptySet.fromList symbols
   in map
         (\code ->
            MS.evalState (playVariants alphabet code) (Rnd.mkStdGen 3141)) .
      MS.evalState
         (replicateM 100 $ replicateM width $
          randomSelect $ NonEmpty.flatten symbols)


withWriteFile :: FilePath -> (IO.Handle -> IO a) -> IO a
withWriteFile path act =
   IO.withFile path IO.WriteMode $ \h ->
      IO.hSetBuffering h IO.LineBuffering >> act h

run :: IO.Handle -> (String, NonEmpty.T [] Char) -> Int -> IO ()
run averagesHandle (alphabetName,alphabet) width = do
   let pathEnding = printf "-%s-%d.csv" alphabetName width
   let games =
         Strategy.withStrategy
            (Strategy.parList $
             Strategy.parTuple2
                Strategy.rdeepseq (Strategy.parTraversable Strategy.rdeepseq)) $
         playMany alphabet width $ Rnd.mkStdGen 42
   let writeLines path txt = withWriteFile path $ \h -> IO.hPutStr h txt
   let csvLine cells = List.intercalate "," $ Fold.toList cells
   let writeCSV path = writeLines path . unlines . map csvLine
   writeCSV ("game-lengths" ++ pathEnding) $
      map (\(code,lengths) -> ('"':code++'"':"") !: fmap show lengths) games
   let gamesPerStrategy = Zip.transposeClip $ map snd games
   writeCSV ("histogram" ++ pathEnding) $ map (fmap show) $
      FuncHT.outerProduct (Map.findWithDefault 0)
         [1 .. NonEmpty.foldl1Map max maximum gamesPerStrategy]
         (fmap histogram gamesPerStrategy)
   let average :: [Int] -> Double
       average xs = fromIntegral (sum xs) / fromIntegral (length xs)
       sqr x = x^(2::Int)
       averageDeviation xs =
         let mean = average xs
         in [show mean,
             printf "%.4f" $ sqrt (average (map sqr xs) - sqr mean)]
   IO.hPutStrLn averagesHandle $ csvLine $
      alphabetName !: show width !:
         Fold.foldMap averageDeviation gamesPerStrategy

main :: IO ()
main =
   withWriteFile "averages.csv" $ \averageHandle ->
   sequence_ $
   liftM2 (run averageHandle)
      [("numbers", '0'!:['1'..'9']), ("words", 'a'!:['b'..'z'])]
      [3..6]