rubyquiz/TicTacToe.hs

312 lines
10 KiB
Haskell
Raw Permalink Normal View History

2012-08-06 15:36:15 +05:30
{-
A learning tic-tac-toe player in Haskell. It learns the game
by playing against itself repeatedly.
It can play against humans too!
A solution to rubyquiz 11 (http://rubyquiz.com/quiz11.html).
2012-09-08 17:03:53 +05:30
Usage: ./TicTacToe board_size training_time
2012-08-06 15:36:15 +05:30
Copyright 2012 Abhinav Sarkar <abhinav@abhinavsarkar.net>
-}
2012-09-08 17:03:53 +05:30
{-# LANGUAGE BangPatterns, RecordWildCards #-}
2012-08-06 01:06:22 +05:30
2012-10-27 11:07:22 +05:30
module TicTacToe (Move(..), CellState(..), Cell(..), Board(..), Run, Result(..),
Player(..), playMatch, playMatches, RandomPlayer(..),
LearningPlayer, learnedPlayer, playHuman, main)
where
2012-08-06 01:06:22 +05:30
2012-09-08 17:03:53 +05:30
import qualified Data.Map as M
import Control.Monad.State (State, get, put, runState, evalState)
2012-08-06 01:06:22 +05:30
import Data.List (sort, nub, maximumBy)
import Data.List.Split (chunksOf)
2012-08-06 01:06:22 +05:30
import Data.Ord (comparing)
2012-09-08 17:03:53 +05:30
import System.Environment (getArgs)
2012-08-06 01:06:22 +05:30
import System.IO (hSetBuffering, stdin, stdout, BufferMode(..))
2012-09-08 17:03:53 +05:30
import System.Random (Random, StdGen, randomR, newStdGen, split)
import Text.Printf (printf)
2012-08-06 01:06:22 +05:30
-- Randomness setup
type RandomState = State StdGen
getRandomR :: Random a => (a, a) -> RandomState a
getRandomR limits = do
gen <- get
let (val, gen') = randomR limits gen
put gen'
return val
randomChoose :: [a] -> RandomState a
randomChoose list = do
i <- getRandomR (0, length list - 1)
return $ list !! i
2012-08-06 15:31:05 +05:30
toss :: RandomState Bool
toss = randomChoose [True, False]
2012-08-06 01:06:22 +05:30
-- Board setup
data Move = Nought | Cross deriving (Eq, Ord)
data CellState = Filled Move | Empty deriving (Eq, Ord)
2012-09-08 17:03:53 +05:30
data Cell = Cell { cellPos :: Int, cellState :: CellState } deriving (Eq, Ord)
2012-08-06 01:06:22 +05:30
2012-09-08 17:03:53 +05:30
data Board = Board { boardSize :: Int, boardCells :: [Cell] } deriving (Eq, Ord)
2012-08-06 01:06:22 +05:30
type Run = [Board]
data Result = Win | Loss | Draw | Unfinished deriving (Eq, Show)
instance Show Move where
show Nought = "O"
show Cross = "X"
instance Show CellState where
show (Filled move) = show move
show Empty = "~"
instance Show Cell where
show c = show $ cellState c
otherMove :: Move -> Move
otherMove Nought = Cross
otherMove Cross = Nought
otherResult :: Result -> Result
otherResult Draw = Draw
otherResult Loss = Win
otherResult Win = Loss
2012-09-08 17:03:53 +05:30
emptyBoard :: Int -> Board
emptyBoard boardSize =
Board boardSize $ map (flip Cell Empty) [0..(boardSize * boardSize - 1)]
2012-08-06 01:06:22 +05:30
printBoard :: Board -> IO ()
printBoard Board{..} = putStrLn "" >> (mapM_ print . chunksOf boardSize $ boardCells)
2012-08-06 01:06:22 +05:30
makeMove :: Int -> Move -> Board -> Board
2012-09-08 17:03:53 +05:30
makeMove pos move board@Board{..} =
let (l, r) = splitAt pos boardCells
in board { boardCells = l ++ [Cell pos (Filled move)] ++ tail r }
2012-08-06 01:06:22 +05:30
diags :: Board -> [[Cell]]
2012-09-08 17:03:53 +05:30
diags Board{..} =
[map (boardCells !!) . take boardSize . iterate (+ (boardSize + 1)) $ 0,
map (boardCells !!) . take boardSize . iterate (+ (boardSize - 1)) $ (boardSize - 1)]
2012-08-06 01:06:22 +05:30
2012-08-06 15:31:05 +05:30
nextBoards :: Move -> Board -> [(Int, Board)]
2012-09-08 17:03:53 +05:30
nextBoards move board@Board{..} =
2012-08-06 15:31:05 +05:30
map ((\p -> (p, makeMove p move board)) . cellPos)
2012-09-08 17:03:53 +05:30
$ filter (\c -> cellState c == Empty) boardCells
2012-08-06 01:06:22 +05:30
isWin :: Move -> Board -> Bool
isWin move board =
or [any isStrike . chunksOf size . map cellState . boardCells $ board,
any isStrike . chunksOf size . map cellState . boardCells . rotateBoard $ board,
2012-09-08 17:03:53 +05:30
any isStrike . map (map cellState) . diags $ board]
2012-08-06 01:06:22 +05:30
where
2012-09-08 17:03:53 +05:30
size = boardSize board
isStrike = (== replicate size (Filled move))
2012-08-06 01:06:22 +05:30
result :: Move -> Board -> Result
result move board
2012-09-08 17:03:53 +05:30
| isWin move board = Win
| isWin (otherMove move) board = Loss
| Empty `elem` (map cellState . boardCells $ board) = Unfinished
| otherwise = Draw
2012-08-06 01:06:22 +05:30
translateBoard :: [Int] -> Board -> Board
2012-09-08 17:03:53 +05:30
translateBoard idxs board@Board{..} =
board { boardCells =
map (\(i, ri) -> Cell i . cellState $ boardCells !! ri)
$ zip [0..(boardSize * boardSize - 1)] idxs }
2012-08-06 01:06:22 +05:30
rotateBoard, xMirrorBoard, yMirrorBoard :: Board -> Board
2012-09-08 17:03:53 +05:30
rotateBoard board@Board{..} =
translateBoard
(let xs = reverse . chunksOf boardSize $ [0..(boardSize * boardSize - 1)]
2012-09-08 17:03:53 +05:30
in concatMap (\i -> map (!! i ) xs) [0..(boardSize - 1)])
board
xMirrorBoard board@Board{..} =
translateBoard
(concatMap reverse . chunksOf boardSize $ [0..(boardSize * boardSize - 1)]) board
2012-09-08 17:03:53 +05:30
yMirrorBoard board@Board{..} =
translateBoard
(concat . reverse . chunksOf boardSize $ [0..(boardSize * boardSize - 1)]) board
2012-08-06 01:06:22 +05:30
rotateBoardN :: Board -> Int -> Board
rotateBoardN board n = foldl (\b _ -> rotateBoard b) board [1..n]
-- Player setup
class Player a where
playerMove :: a -> Move
play :: a -> Board -> (a, Board)
improvePlayer :: a -> Result -> Run -> a
2012-08-06 15:31:05 +05:30
-- play a match between two players
2012-09-08 17:03:53 +05:30
playMatch :: (Player p1, Player p2) => p1 -> p2 -> Board -> (Result, Run, p1, p2)
playMatch player1 player2 board =
2012-08-06 01:06:22 +05:30
case result (playerMove player1) board of
Unfinished -> let
(player1', board') = play player1 board
in case result (playerMove player1) board' of
2012-08-06 15:31:05 +05:30
Unfinished -> let
2012-09-08 17:03:53 +05:30
(res', run, player2', player1'') = playMatch player2 player1' board'
2012-08-06 15:31:05 +05:30
in (otherResult res', board' : run, player1'', player2')
res -> (res, [], player1', player2)
2012-08-06 01:06:22 +05:30
res -> (res, [], player1, player2)
2012-08-06 15:31:05 +05:30
-- play multiple matches between two players
2012-09-08 17:03:53 +05:30
playMatches :: (Player p1, Player p2) => Int -> Int -> p1 -> p2
-> ([(Result, Run)],p1, p2)
playMatches boardSize times player1 player2 =
2012-08-06 01:06:22 +05:30
foldl (\(matches, p1, p2) _ ->
let
2012-09-08 17:03:53 +05:30
startBoard = emptyBoard boardSize
(res, run, p1', p2') = playMatch p1 p2 startBoard
2012-08-06 01:06:22 +05:30
p1'' = improvePlayer p1' res run
p2'' = improvePlayer p2' (otherResult res) run
in ((res, run) : matches, p1'', p2''))
([], player1, player2) [1..times]
-- RandomPlayer setup
2012-08-06 15:31:05 +05:30
-- play randomly. choose a random move
2012-08-06 01:06:22 +05:30
randomPlay :: Move -> Board -> RandomState Board
2012-08-06 15:31:05 +05:30
randomPlay move board = randomChoose (map snd $ nextBoards move board)
2012-08-06 01:06:22 +05:30
data RandomPlayer = RandomPlayer Move StdGen deriving (Show)
instance Player RandomPlayer where
playerMove (RandomPlayer move _) = move
play (RandomPlayer move gen) board =
let
(board', gen') = runState (randomPlay move board) gen
in (RandomPlayer move gen', board')
improvePlayer player _ _ = player
-- LearningPlayer setup
type Memory = M.Map Board (Int, Int, Int)
2012-08-06 15:31:05 +05:30
-- boards equivalent to this board
2012-08-06 01:06:22 +05:30
eqvBoards :: Board -> [Board]
eqvBoards board = nub . sort $
board : map (rotateBoardN board) [1..3] ++ [xMirrorBoard board, yMirrorBoard board]
2012-09-08 17:03:53 +05:30
data LearningPlayer = LearningPlayer Move Memory StdGen
2012-08-06 01:06:22 +05:30
2012-08-06 15:31:05 +05:30
-- play using the strategy learned till now
2012-08-06 01:06:22 +05:30
learningPlay :: LearningPlayer -> Board -> (LearningPlayer, Board)
2012-08-06 15:31:05 +05:30
learningPlay (LearningPlayer move mem gen) board = let
next = map snd $ nextBoards move board
in case filter (isWin move) next of
(winBoard:_) -> (LearningPlayer move mem gen, winBoard)
[] -> let
otherNext = nextBoards (otherMove move) board
in case filter (isWin (otherMove move) . snd) otherNext of
((pos,_):_) -> (LearningPlayer move mem gen, makeMove pos move board)
[] -> let
2012-08-06 16:47:31 +05:30
scores = map (\b -> (b, boardScore b mem)) next
2012-08-06 15:31:05 +05:30
(board', (w, _, d)) = maximumBy (comparing (calcScore . snd)) scores
in if w /= 0
then (LearningPlayer move mem gen, board')
else let
((rBoard, _), gen') = runState (randomChoose scores) gen
in (LearningPlayer move mem gen', rBoard)
2012-08-06 01:06:22 +05:30
where
boardScore board' mem =
foldl (\score b' -> sumScores score $ M.findWithDefault (0, 0, 0) b' mem)
(0, 0, 0) (eqvBoards board')
sumScores (w, l, d) (w', l', d') = (w + w', l + l', d + d')
2012-08-06 15:31:05 +05:30
calcScore :: (Int, Int, Int) -> Double
calcScore (w, l, d) = fromIntegral w + fromIntegral d * 0.5 - fromIntegral l
2012-08-06 01:06:22 +05:30
2012-08-06 15:31:05 +05:30
-- learn strategy from the run
learnFromRun :: Result -> Run -> Memory -> Memory
learnFromRun res run mem = let
2012-08-06 01:06:22 +05:30
score = incrementScore res (0, 0, 0)
mem' = foldl (\m b -> M.insertWith (\_ -> incrementScore res) b score m)
mem run
in mem'
where
incrementScore res (w, l, d) =
case res of
Win -> (w + 1, l, d)
Loss -> (w, l + 1, d)
Draw -> (w, l, d + 1)
instance Player LearningPlayer where
playerMove (LearningPlayer move _ _) = move
play = learningPlay
improvePlayer (LearningPlayer move mem gen) res run =
2012-08-06 15:31:05 +05:30
LearningPlayer move (learnFromRun res run mem) gen
2012-08-06 01:06:22 +05:30
2012-08-06 15:31:05 +05:30
-- play two LearningPlayers against each other to learn strategy
2012-09-08 17:03:53 +05:30
learnedPlayer :: Int -> Int -> Move -> StdGen -> LearningPlayer
learnedPlayer boardSize times move gen = let
2012-08-06 01:06:22 +05:30
(gen1, gen2) = split gen
p1 = LearningPlayer move M.empty gen1
p2 = LearningPlayer (otherMove move) M.empty gen2
2012-09-08 17:03:53 +05:30
(_, p1', p2') = playMatches boardSize times p1 p2
2012-08-06 01:06:22 +05:30
in p1'
-- Play against human
2012-08-06 15:31:05 +05:30
-- play a player against a human. human enters moves from prompt.
playHuman :: Player p => p -> Board -> IO ()
2012-09-08 17:03:53 +05:30
playHuman player board@Board{..} = do
2012-08-06 01:06:22 +05:30
printBoard board
2012-09-08 17:03:53 +05:30
let boardArea = boardSize * boardSize
2012-08-06 01:06:22 +05:30
case result (playerMove player) board of
Unfinished -> do
2012-09-08 17:03:53 +05:30
putStr $ printf "Move %s? [1-%s] "
(show . otherMove . playerMove $ player) (show boardArea)
2012-08-06 15:31:05 +05:30
pos <- fmap (decr . read) getLine
2012-09-08 17:03:53 +05:30
if pos < 0 || pos > (boardArea - 1)
2012-08-06 01:06:22 +05:30
then do
putStrLn "Invalid Move"
playHuman player board
else
2012-09-08 17:03:53 +05:30
case cellState (boardCells !! pos) of
2012-08-06 01:06:22 +05:30
Filled _ -> do
putStrLn "Invalid Move"
playHuman player board
Empty -> let
board' = makeMove pos Nought board
in case result (playerMove player) board' of
Unfinished -> let
(player', board'') = play player board'
in playHuman player' board''
res -> do
printBoard board'
putStrLn ("Your " ++ show (otherResult res))
res -> putStrLn ("Your " ++ show (otherResult res))
2012-08-06 15:31:05 +05:30
where decr x = x - 1
2012-08-06 01:06:22 +05:30
main :: IO ()
main = do
2012-09-08 17:03:53 +05:30
(boardSize : times : _) <- fmap (map read) getArgs
2012-08-06 01:06:22 +05:30
hSetBuffering stdin LineBuffering
hSetBuffering stdout NoBuffering
gen <- newStdGen
putStrLn "Learning ..."
2012-09-08 17:03:53 +05:30
let !player = learnedPlayer boardSize times Cross gen
2012-08-06 01:06:22 +05:30
putStrLn "Learned"
2012-08-06 15:31:05 +05:30
putStrLn "Tossing for first move"
let t = evalState toss gen
2012-09-08 17:03:53 +05:30
let startBoard = emptyBoard boardSize
2012-08-06 15:31:05 +05:30
if t
then do
putStrLn "You win toss"
2012-09-08 17:03:53 +05:30
playHuman player startBoard
2012-08-06 15:31:05 +05:30
else do
putStrLn "You lose toss"
2012-09-08 17:03:53 +05:30
let (player', board) = play player startBoard
2012-08-06 15:31:05 +05:30
playHuman player' board