packages feed

reactive-banana-threepenny-0.7.1.3: src/TicTacToe.hs

{-----------------------------------------------------------------------------
    reactive-banana-threepenny
    
    Example: TicTacToe.
    Original Author: Gideon Sireling
------------------------------------------------------------------------------}
{-# LANGUAGE ScopedTypeVariables #-} -- allows "forall t. Moment t"

import Control.Monad
import Data.Array
import Data.List hiding (union)
import Data.Maybe

import qualified Graphics.UI.Threepenny as UI
import Graphics.UI.Threepenny.Core hiding (Event, filterJust)

import Reactive.Banana
import Reactive.Banana.Threepenny

import Paths

{-----------------------------------------------------------------------------
    User Interface
------------------------------------------------------------------------------}
main :: IO ()
main = do
    startGUI Config
        { tpPort       = 10000
        , tpCustomHTML = Nothing
        , tpStatic     = ""
        } setup

-- | Load images corresponding to tokens.
loadToken2URL :: Window -> IO (Token -> String)
loadToken2URL window = do
    let loadPng name = loadFile window "image/png" =<< getDataFile (name ++ ".png")
    [none, x, o] <- mapM loadPng ["empty","cross","circle"]
    return $ \token -> case token of { None -> none; X -> x; O -> o; }

-- | Set up user interface.
setup :: Window -> IO ()
setup window = do
    return window # set title "Tic Tac Toe"

    token2url <- loadToken2URL window
    
    status <- UI.div # set text "Player: X"
    let repeat n = sequence . replicate n
    fields <- repeat 3 $ repeat 3 $
        UI.img # set UI.src (token2url None) # set style [("border","1px solid black")]
    
    getBody window #+ [grid $ map (map element) fields, element status] 


    let networkDescription :: forall t. Frameworks t => Moment t ()
        networkDescription = do
            events <- concat <$> mapM (mapM (event UI.click)) fields
            
            let
                moves :: Event t (Game -> Game)
                moves = foldl1 union $ zipWith (\e s -> move s <$ e) events
                        [(x,y) | y <- [1..3], x <- [1..3]]
                
                state :: Behavior t Game
                state = accumB newGame moves

                currentPlayer :: Behavior t Token
                currentPlayer = player <$> state
                
                tokens :: [Behavior t Token]
                tokens = map (\e -> stepper None (currentPlayer <@ e)) events
        
            zipWithM_
                (\field token -> return field # sink UI.src (token2url <$> token))
                (concat fields)
                tokens
        
            return status # sink text (("Player: " ++) . show <$> currentPlayer)
            
            -- end game event handler
            eState <- changes state
            reactimate $ end window <$> filterJust (isGameEnd . board <$> eState)
    
    network <- compile networkDescription    
    actuate network


end :: Window -> Token -> IO ()
end window result = do
    alert window $
        case result of
            X    -> "X won!"
            O    -> "O won!"
            None -> "Draw!"
    clear window

alert w s = runFunction w (ffi "alert(%1)" s)

{-----------------------------------------------------------------------------
    Game Logic
------------------------------------------------------------------------------}
data Token = None | X | O
    deriving Eq

-- |The coordinates of a square.
type Square = (Int,Int)

-- |A noughts and crosses board.
type Board = Array Square Token

-- |Returns an empty 'Board'.
newBoard :: Board
newBoard = listArray ((1,1),(3,3)) (repeat None)

-- |Puts a 'Token' in a 'Square'.
setSquare :: Board -> Square -> Token -> Board
setSquare board square token =
    if (board ! square) /= None
    then error $ "square " ++ show square ++ " is not empty"
    else board // [(square, token)]

-- |Determine if the 'Board' is in an end state.
--  Returns 'Just' 'Token' if the game has been won,
-- 'Just' 'None' for a draw, otherwise 'Nothing'.
isGameEnd :: Board -> Maybe Token
isGameEnd board
    | Just X `elem` maybeWins = Just X
    | Just O `elem` maybeWins = Just O
    | None `notElem` elems board = Just None
    | otherwise = Nothing

    where rows :: [[Square]]
          rows = let i = [1..3]
                 in [[(x,y) | y <- i] | x <- i] ++ -- rows
                    [[(y,x) | y <- i] | x <- i] ++ -- coloumns
                    [[(x,x) | x <- i], [(x,4-x) | x <- i]] -- diagonals

          rows2tokens :: [[Token]]
          rows2tokens = map (map (board !)) rows

          isWin :: [Token] -> Maybe Token
          isWin tokens
              | all (==X) tokens = Just X
              | all (==O) tokens = Just O
              | otherwise = Nothing

          maybeWins :: [Maybe Token]
          maybeWins = map isWin rows2tokens

-- |The state of a game, i.e. the player who's turn it is, and the current board.
data Game = Game { player :: Token, board :: Board }

newGame :: Game
newGame = Game X newBoard

-- |Puts the player's token on the specified square.
-- Returns 'Just' 'Token' if the game has been won,
-- 'Just' 'None' for a draw, otherwise 'Nothing'.
move :: Square -> Game -> Game
move square (Game player board) = Game player' board'
    where
    board'  = setSquare board square player
    player' = case player of {X -> O; O -> X}

{-----------------------------------------------------------------------------
    Show instances
------------------------------------------------------------------------------}
outersperse :: a -> [a] -> [a]
outersperse x ys = x : intersperse x ys ++ [x]

instance Show Token where
    show X = "X"
    show O = "O"
    show None = " "
    showList tokens = showString $ outersperse '|' $ concatMap show tokens

-- Board cannot be declared an instance of Show,
-- as this would overlap with the existing instance for Array.
showBoard :: Board -> String
showBoard board =
    let border = " +-+-+-+"
        i = [1..3]
        showRow x = show x ++ show [board ! (y,x) | y <- i]
    in intercalate "\n" $ "  1 2 3" : outersperse border (map showRow i)

instance Show Game where
    show (Game player board) = showBoard board ++ "\n\nTurn: " ++ show player