File: TicTacToe.hs

package info (click to toggle)
haskell-operational 0.2.3.2-1
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd
  • size: 132 kB
  • sloc: haskell: 441; sh: 78; makefile: 2
file content (175 lines) | stat: -rw-r--r-- 5,756 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
{------------------------------------------------------------------------------
    Control.Monad.Operational
    
    Example:
    An implementation of the game TicTacToe.
    
    Each player (human, AI, ...) is implemented in a separate monad
    which are then intermingled to run the game. This resembles the
    PoorMansConcurrency.hs example.
    
    
    Many thanks to Yves Par`es and Bertram Felgenhauer
    http://www.haskell.org/pipermail/haskell-cafe/2010-April/076216.html

------------------------------------------------------------------------------}
{-# LANGUAGE GADTs, Rank2Types #-}

import Control.Monad
import Control.Monad.Operational
import Control.Monad.State

import Data.Either
import Data.List

    -- external libraries needed
import System.Random

{------------------------------------------------------------------------------
    The Player monad for implementing players (human, AI, ...)
    provides two operations
    
        readBoard   -- read the current board position
        playMove    -- play a move

    to query the current board position and perform a move, respectively.
    
    Moreover, it's actually a monad transformer intended to be used over IO.
    This way, the players can perform IO computations.
------------------------------------------------------------------------------}
data PlayerI a where
    ReadBoard :: PlayerI Board
    PlayMove  :: Int -> PlayerI Bool
    
type Player m a = ProgramT PlayerI m a

readBoard = singleton ReadBoard
playMove  = singleton . PlayMove

    -- interpreter
runGame :: Player IO () -> Player IO () -> IO ()
runGame player1 player2 = eval' initialGameState player1 player2
    where
    eval' game p1 p2 = viewT p1 >>= \p1view -> eval game p1view p2
    
    eval :: GameState
         -> ProgramViewT PlayerI IO () -> Player IO ()
         -> IO ()
    eval game (Return _)            _  = return ()
    eval game (ReadBoard   :>>= p1) p2 = eval' game (p1 (board game)) p2
    eval game (PlayMove mv :>>= p1) p2 =
        case makeMove mv game of
            Nothing         -> eval' game (p1 False) p2
            Just game'
                | won game' -> let p = activePlayer game in
                               putStrLn $ "Player " ++ show p ++ " has won!"
                | draw game'-> putStrLn $ "It's a draw."
                | otherwise -> eval' game' p2 (p1 True)
    
    -- example: human vs AI
main = do
    g <- getStdGen
    runGame playerHuman (playerAI g)

{------------------------------------------------------------------------------
    TicTacToe Board type and logic
    
    The board looks like this:
    
    +---+---+---+   some squares already played on
    | 1 | 2 | 3 |   the empty squares are numbered
    +---+---+---+
    | 4 | 5 |OOO|
    +---+---+---+
    | 7 |XXX| 9 |
    +---+---+---+
------------------------------------------------------------------------------}
data Symbol = X | O deriving (Eq,Show)
type Square = Either Int Symbol
type Board = [[Square]]
data GameState = Game { board :: Board, activePlayer :: Symbol }

initialGameState :: GameState
initialGameState = Game (map (map Left) [[1,2,3],[4,5,6],[7,8,9]]) X

    -- list the possible moves to play
possibleMoves :: Board -> [Int]
possibleMoves board = [k | Left k <- concat board]

    -- play a stone at a square
makeMove :: Int -> GameState -> Maybe GameState
makeMove k (Game board player)
    | not (k `elem` possibleMoves board) = Nothing   -- illegal move
    | otherwise = Just $ Game (map (map replace) board) (switch player)
    where
    replace (Left k') | k' == k = Right player
    replace x                   = x

    switch X = O
    switch O = X

    -- has somebody won the game?
won :: GameState -> Bool
won (Game board _) = any full $ diagonals board ++ rows board ++ cols board
    where
    full [a,b,c] = a == b && b == c
    diagonals [[a1,_,b1],
               [_ ,c,_ ],
               [b2,_,a2]] = [[a1,c,a2],[b1,c,b2]]
    rows = id
    cols = transpose

    -- is the game a draw?
draw :: GameState -> Bool
draw (Game board _) = null (possibleMoves board)

    -- print the board
showSquare = either (\n -> " " ++ show n ++ " ") (concat . replicate 3 . show)

showBoard :: Board -> String
showBoard board =
      unlines . surround "+---+---+---+"
    . map (concat . surround "|". map showSquare)
    $ board
    where
    surround x xs = [x] ++ intersperse x xs ++ [x]

printBoard = putStr . showBoard

{------------------------------------------------------------------------------
    Player examples
------------------------------------------------------------------------------}
    -- a human player on the command line
playerHuman :: Player IO ()
playerHuman = forever $ readBoard >>= liftIO . printBoard >> doMove
    where
    -- ask the player where to move
    doMove :: Player IO ()
    doMove = do
        liftIO . putStrLn $ "At which number would you like to play?"
        n <- liftIO getLine
        b <- playMove (read n)
        unless b $ do
            liftIO . putStrLn $ "Position " ++ show n ++ " is already full."
            doMove

    -- a random AI,
    -- also demonstrates how to use a custom StateT on top
    --   of the Player monad
playerAI :: Monad m => StdGen -> Player m ()
playerAI = evalStateT ai
    where
    ai :: Monad m => StateT StdGen (ProgramT PlayerI m) ()
    ai = forever $ do
        board <- lift $ readBoard
        n     <- uniform (possibleMoves board) -- select a random move
        lift $ playMove n
        where
        -- select one element at random
        uniform :: Monad m => [a] -> StateT StdGen m a
        uniform xs = do
            gen <- get
            let (n,gen') = randomR (1,length xs) gen
            put gen'
            return (xs !! (n-1))