| @@ -0,0 +1,299 @@ | |||||
| {-# LANGUAGE MultiParamTypeClasses #-} | |||||
| {-# LANGUAGE TypeSynonymInstances #-} | |||||
| {-# LANGUAGE FlexibleInstances #-} | |||||
| {-# LANGUAGE FlexibleContexts #-} | |||||
| {-# LANGUAGE FunctionalDependencies #-} | |||||
| {-# LANGUAGE TupleSections #-} | |||||
| module Skat.AI.Minmax ( | |||||
| choose, playCLI | |||||
| ) where | |||||
| import Control.Monad.State | |||||
| import Control.Monad.Fail | |||||
| import Data.Ord | |||||
| import Text.Read (readMaybe) | |||||
| import Data.List (minimumBy, maximumBy) | |||||
| import Debug.Trace | |||||
| import qualified Skat as S | |||||
| import qualified Skat.Card as S | |||||
| import qualified Skat.Operations as S | |||||
| import qualified Skat.Pile as S | |||||
| import qualified Skat.Player as S | |||||
| import qualified Skat.Render as S | |||||
| debug :: Bool | |||||
| debug = False | |||||
| class (Ord v, Eq v) => Value v where | |||||
| invert :: v -> v | |||||
| win :: v | |||||
| loss :: v | |||||
| class Player p where | |||||
| maxing :: p -> Bool | |||||
| class (Monad m, Value v, Player p, Eq t) => MonadGame t v p m | m -> t, m -> p, m -> v where | |||||
| currentPlayer :: m p | |||||
| turns :: m [t] | |||||
| play :: t -> m () | |||||
| simulate :: t -> m a -> m a | |||||
| evaluate :: m v | |||||
| over :: m Bool | |||||
| class (MonadIO m, Show t, Show v, Show p, MonadGame t v p m) => PlayableGame t v p m | m -> t, m -> p, m -> v where | |||||
| showTurns :: m () | |||||
| showBoard :: m () | |||||
| askTurn :: m (Maybe t) | |||||
| showTurn :: t -> m () | |||||
| winner :: m (Maybe p) | |||||
| -- Skat implementation | |||||
| instance Player S.PL where | |||||
| maxing p = S.team p == S.Team | |||||
| instance Value Int where | |||||
| invert = negate | |||||
| win = 120 | |||||
| loss = -120 | |||||
| instance MonadGame S.Card Int S.PL S.Skat where | |||||
| currentPlayer = do | |||||
| hand <- gets S.currentHand | |||||
| pls <- gets S.players | |||||
| return $ S.player pls hand | |||||
| turns = S.allowedCards | |||||
| play = S.play_ | |||||
| simulate card action = do | |||||
| backup <- get | |||||
| play card | |||||
| res <- action | |||||
| put backup | |||||
| return res | |||||
| over = ((==0) . length) <$> S.allowedCards | |||||
| evaluate = do | |||||
| player <- currentPlayer | |||||
| piles <- gets S.piles | |||||
| let (sgl, tm) = S.count piles | |||||
| return $ (if maxing player then tm - sgl else sgl - tm) | |||||
| -- TIC TAC TOE implementation | |||||
| data TicTacToe = Tic | Tac | Toe | |||||
| deriving (Eq, Ord) | |||||
| instance Show TicTacToe where | |||||
| show Tic = "O" | |||||
| show Tac = "X" | |||||
| show Toe = "_" | |||||
| data WinLossTie = Loss | Tie | Win | |||||
| deriving (Eq, Show, Ord) | |||||
| instance Value WinLossTie where | |||||
| invert Win = Loss | |||||
| invert Loss = Win | |||||
| invert Tie = Tie | |||||
| win = Win | |||||
| loss = Loss | |||||
| data GameState = GameState { getBoard :: [TicTacToe] | |||||
| , getCurrent :: Bool } | |||||
| deriving Show | |||||
| instance Player Bool where | |||||
| maxing = id | |||||
| instance Monad m => MonadGame Int WinLossTie Bool (StateT GameState m) where | |||||
| currentPlayer = gets getCurrent | |||||
| turns = do | |||||
| board <- gets getBoard | |||||
| let fields = zip [0..] board | |||||
| return $ map fst $ filter ((==Toe) . snd) fields | |||||
| play turn = do | |||||
| env <- get | |||||
| let value = if getCurrent env then Tic else Tac | |||||
| board' = updateAt turn (getBoard env) value | |||||
| current' = not $ getCurrent env | |||||
| put $ GameState board' current' | |||||
| simulate turn action = do | |||||
| backup <- get | |||||
| play turn | |||||
| res <- action | |||||
| put backup | |||||
| return res | |||||
| evaluate = do | |||||
| board <- gets getBoard | |||||
| current <- currentPlayer | |||||
| let mayWinner = ticWinner board | |||||
| case mayWinner of | |||||
| Just Tic -> return $ if current then Win else Loss | |||||
| Just Tac -> return $ if current then Loss else Win | |||||
| Just Toe -> return Tie | |||||
| Nothing -> return Tie | |||||
| over = do | |||||
| board <- gets getBoard | |||||
| case ticWinner board of | |||||
| Just _ -> return True | |||||
| _ -> return False | |||||
| ticWinner :: [TicTacToe] -> Maybe TicTacToe | |||||
| ticWinner board | |||||
| | ticWon = Just Tic | |||||
| | tacWon = Just Tac | |||||
| | over = Just Toe | |||||
| | otherwise = Nothing | |||||
| where ticWon = hasWon $ map (==Tic) board | |||||
| tacWon = hasWon $ map (==Tac) board | |||||
| hasWon (True:_:_:True:_:_:True:_:_:[]) = True | |||||
| hasWon (True:_:_:_:True:_:_:_:True:[]) = True | |||||
| hasWon (_:True:_:_:True:_:_:True:_:[]) = True | |||||
| hasWon (_:_:True:_:_:True:_:_:True:[]) = True | |||||
| hasWon (_:_:True:_:True:_:True:_:_:[]) = True | |||||
| hasWon (True:True:True:_:_:_:_:_:_:[]) = True | |||||
| hasWon (_:_:_:True:True:True:_:_:_:[]) = True | |||||
| hasWon (_:_:_:_:_:_:True:True:True:[]) = True | |||||
| hasWon _ = False | |||||
| over = (length $ filter (==Toe) board) == 0 | |||||
| updateAt :: Int -> [a] -> a -> [a] | |||||
| updateAt n xs y = map f $ zip [0..] xs | |||||
| where f (i, x) = if i == n then y else x | |||||
| minmax :: (MonadIO m, Show v, Show t, Show p, Value v, Eq t, Player p, MonadGame t v p m) | |||||
| => Int | |||||
| -> t | |||||
| -> v | |||||
| -> v | |||||
| -> m (t, v) | |||||
| minmax depth turn alpha beta = (flip evalStateT) (alpha, beta) $ do | |||||
| gameOver <- lift over | |||||
| -- if last step or game is over then evaluate situation | |||||
| if depth == 0 || gameOver then do | |||||
| val <- lift evaluate | |||||
| when debug $ liftIO $ putStrLn $ "evaluation: " ++ show val | |||||
| return (turn, val) | |||||
| else do | |||||
| when debug $ liftIO $ putStrLn $ "depth " ++ show depth | |||||
| -- generate a list of possible turns | |||||
| currentlyMaxing <- maxing <$> lift currentPlayer | |||||
| availableTurns <- lift turns | |||||
| (alpha, beta) <- get | |||||
| -- try every turn, StateT wraps current best turn and current max value | |||||
| (flip execStateT) (undefined, alpha) $ forM_ availableTurns $ \turn -> do | |||||
| currentMax <- gets snd | |||||
| when debug $ liftIO $ putStrLn $ "simulating " ++ show turn ++ " with max " ++ show currentMax | |||||
| ++ " and beta " ++ show beta | |||||
| --when (currentMax >= beta && debug) $ liftIO $ putStrLn "beta cutoff" | |||||
| -- beta cutoff | |||||
| unless (currentMax >= beta) $ do | |||||
| --unless False $ do | |||||
| value <- lift $ lift $ simulate turn $ step currentlyMaxing beta currentMax | |||||
| when debug $ liftIO $ putStrLn $ "value " ++ show value | |||||
| when (value > currentMax) (put (turn, value)) | |||||
| where step currentlyMaxing beta currentMax = do | |||||
| nextMaxing <- maxing <$> currentPlayer | |||||
| if nextMaxing /= currentlyMaxing | |||||
| then (invert . snd) <$> minmax (depth-1) turn (invert beta) (invert currentMax) | |||||
| else snd <$> minmax (depth-1) turn currentMax beta | |||||
| choose :: (MonadIO m, Show v, Show t, Show p, Value v, Eq t, Player p, MonadGame t v p m) => m t | |||||
| choose = fst <$> minmax 10 undefined loss win | |||||
| emptyBoard :: [TicTacToe] | |||||
| emptyBoard = [Toe, Toe, Toe, Toe, Toe, Toe, Toe, Toe, Toe] | |||||
| otherBoard :: [TicTacToe] | |||||
| otherBoard = [Tic, Tac, Tac, Tic, Tac, Tic, Toe, Tic, Toe] | |||||
| print9x9 :: (Int -> IO ()) -> IO () | |||||
| print9x9 pr = pr 0 >> pr 1 >> pr 2 >> putStrLn "" | |||||
| >> pr 3 >> pr 4 >> pr 5 >> putStrLn "" | |||||
| >> pr 6 >> pr 7 >> pr 8 >> putStrLn "" | |||||
| printBoard :: [TicTacToe] -> IO () | |||||
| printBoard board = print9x9 pr >> putStrLn "" | |||||
| where pr n = putStr (show $ board !! n) >> putStr " " | |||||
| printOptions :: [Int] -> IO () | |||||
| printOptions opts = print9x9 pr | |||||
| where pr n | |||||
| | n `elem` opts = putStr (show n) >> putStr " " | |||||
| | otherwise = putStr " " | |||||
| instance MonadIO m => PlayableGame Int WinLossTie Bool (StateT GameState m) where | |||||
| showBoard = do | |||||
| board <- gets getBoard | |||||
| liftIO $ printBoard board | |||||
| showTurns = turns >>= liftIO . printOptions | |||||
| winner = do | |||||
| board <- gets getBoard | |||||
| let win = ticWinner board | |||||
| case win of | |||||
| Just Toe -> return Nothing | |||||
| Just Tic -> return $ Just True | |||||
| Just Tac -> return $ Just False | |||||
| Nothing -> return Nothing | |||||
| askTurn = readMaybe <$> liftIO getLine | |||||
| showTurn _ = return () | |||||
| instance PlayableGame S.Card Int S.PL S.Skat where | |||||
| showBoard = do | |||||
| liftIO $ putStrLn "" | |||||
| table <- S.getp S.tableCards | |||||
| liftIO $ putStr "Table: " | |||||
| liftIO $ print table | |||||
| showTurns = do | |||||
| cards <- turns | |||||
| player <- currentPlayer | |||||
| liftIO $ print player | |||||
| liftIO $ S.render (S.sortRender cards) | |||||
| winner = do | |||||
| piles <- gets S.piles | |||||
| pls <- gets S.players | |||||
| let res = S.count piles :: (Int, Int) | |||||
| winnerTeam = trace (show res) $ if fst res > snd res then S.Single else S.Team | |||||
| winners = filter ((==winnerTeam) . S.team) (S.playersToList pls) | |||||
| return $ Just $ head winners | |||||
| askTurn = do | |||||
| cards <- turns | |||||
| let sorted = S.sortRender cards | |||||
| input <- liftIO getLine | |||||
| case readMaybe input of | |||||
| Just n -> if n >= 0 && n < length sorted then return $ Just (sorted !! n) | |||||
| else return Nothing | |||||
| Nothing -> return Nothing | |||||
| showTurn card = do | |||||
| player <- currentPlayer | |||||
| liftIO $ putStrLn $ show player ++ " plays " ++ show card | |||||
| playCLI :: (MonadFail m, Read t, PlayableGame t v p m) => m () | |||||
| playCLI = do | |||||
| gameOver <- over | |||||
| if gameOver | |||||
| then announceWinner | |||||
| else do | |||||
| showBoard | |||||
| current <- currentPlayer | |||||
| turn <- if not (maxing current) then readTurn else choose | |||||
| showTurn turn | |||||
| play turn | |||||
| playCLI | |||||
| where | |||||
| readTurn = do | |||||
| options <- turns | |||||
| showTurns | |||||
| liftIO $ putStr "> " | |||||
| mayTurn <- askTurn | |||||
| case mayTurn of | |||||
| Just val -> if val `elem` options then return val else readTurn | |||||
| Nothing -> readTurn | |||||
| announceWinner = do | |||||
| showBoard | |||||
| win <- winner | |||||
| liftIO $ putStrLn $ show win ++ " wins the game!" | |||||
| playTicTacToe :: IO () | |||||
| playTicTacToe = void $ (flip runStateT) (GameState emptyBoard True) playCLI | |||||