packages feed

babylon-0.3: GUI.hs

{- 
   GUI for the Babylon game
   Uses wxHaskell bindings to the wxWidgets toolkit
   Pedro Vasconcelos, 2009-2010
-}
module GUI where
import Graphics.UI.WX hiding (play)
import Graphics.UI.WXCore
import Data.Maybe
import System.Random
import Utils (shuffleIO)
import Babylon

import Paths_babylon   -- Cabal locations of image files

data Player = Human | Computer deriving (Eq,Show)

-- list of gamplay levels 
-- "hardest" plays optimally (always wins as 2nd player)
levels :: [(String,Int)]
levels = [("Easy",2), ("Medium",4), ("Hard",8), ("Hardest",12)]

defaultLevel :: Int
defaultLevel = 2


data Game = Game { gameWindow :: Panel ()
                 , gameBitmaps :: [Bitmap ()]
                 , gameBoard :: Var Board
                 , gamePlayer :: Var (Maybe Player)
                 , gameLevel :: Var Int
                 , gameSel :: Var Int
                 , drawnRects :: Var [Rect]
                 }

instance Widget Game where
    widget g = widget (gameWindow g)

instance Visible Game where
    refresh g = do set w [layout := space 720 200]
                   refresh w
        where w = gameWindow g

instance Paint Game where
    repaint g = repaint (gameWindow g)
    paint = error "paint not defined for Game instance"
    paintRaw = error "paintRaw not defined for Game instance"


instance Selection Game where
    selection = newAttr "selection" getter setter
        where getter g = varGet (gameSel g)
              setter g n = do varSet (gameSel g) n
                              repaint g


board :: Attr Game Board
board = newAttr "board" getter setter
    where getter = varGet . gameBoard 
          setter g b = do varSet (gameBoard g) b
                          varSet (gameSel g) (-1)
                          repaint g

player :: Attr Game (Maybe Player)
player = newAttr "player" getter setter
    where getter = varGet . gamePlayer 
          setter = varSet . gamePlayer 


level :: Attr Game Int
level = newAttr "level" getter setter
    where getter = varGet . gameLevel
          setter = varSet . gameLevel



newGame :: Frame a -> StatusField -> IO Game
newGame f sf
    = do bmps <- loadBitmaps
         w <- panel f []
         b <- shuffleIO initialBoard
         vb <- varCreate b
         vrects <- varCreate []
         vplayer <- varCreate (Just Human)
         vlevel <- varCreate defaultLevel
         vsel<- varCreate (-1)
         let g = Game { gameWindow=w
                      , gameBoard=vb
                      , gameBitmaps=bmps
                      , gamePlayer=vplayer
                      , gameLevel=vlevel
                      , gameSel=vsel
                      , drawnRects=vrects
                      }
         set w  [on resize := repaint w,
                 on paint := drawBoard g,
                 on click :=  humanPlay g sf]
         refresh g
         repaint w
         return g
         


-- start the GUI
gui :: IO ()
gui = do f <- frame [text := "Babylon"]
         status <- statusField [text := "Welcome to Babylon"]
         game <- newGame f status
       
         menu<-menuPane [text := "&Game"]
         new<-menuItem menu [text := "New",
                             help := "Restart the game"
                            ]
         menuLine menu
         menuQuit menu [help := "Quit this program", 
                        on command := close f]
         opt <- menuPane [text := "Options"]
         r0<-menuItem opt [text := "Human plays first", 
                           help := "Choose starting player",
                           checkable:=True, checked := True]
         menuLine opt
         -- radio buttons for chosing play level
         rs<-sequence [menuRadioItem opt 
                       [text := txt,  
                        help := "Choose computer opponent level",
                        on command := set game [level:=l]]
                       | (txt,l)<-levels]

         sequence_ [set r [checked:=True] 
                    | (r,l)<-zip rs (map snd levels), l==defaultLevel]

         hlp <- menuHelp []
         rules<- menuItem hlp [text := "Rules", 
                               help := "About the game rules",
                               on command := infoRules f]
         about <- menuAbout hlp [help := "About this program",
                                 on command := infoAbout f]

         -- timer event to periodically play the computer AI 
         t <- timer f [interval := 2000,
                       on command := computerPlay game status]

         set f [statusBar := [status],
                menuBar := [menu, opt, hlp],
                layout := alignCenter (widget game)
               ]

         -- menu handler for restarting a game
         set new [on command := do b <- shuffleIO initialBoard 
                                   c <- get r0 checked
                                   let p = if c then Human else Computer
                                   set game [board := b, player := Just p]
                                   updateStatus status game
                 ]

        

         
-- display an info dialog 
infoAbout w 
    = infoDialog w "About Babylon" $ 
      init $ 
      unlines ["Written in Haskell using the wxWidgets toolkit",
               "by Pedro Vasconcelos <pbv@dcc.fc.up.pt>",
               "",
               "Based on the board game by Bruno Faidutti",
               "Published by FoxMind Games."
              ]

-- display the game rules
infoRules w 
    = infoDialog w "Rules of Babylon" $
      init $ 
      unlines ["Two players take turns moving stacks of colored tiles.",
               "A move is valid provided that:",
                "1) the stacks have same height; or",
                "2) the stacks have the same top color.",
                "The first player who cannot make a move loses the game!"]

-- load bitmaps for the colored tiles
-- uses Cabal to get portable file paths
loadBitmaps :: IO [Bitmap ()]
loadBitmaps
    = sequence [do { f<-getDataFileName ("images/" ++ show c ++ ".png")
                   ; bitmapCreateLoad f wxBITMAP_TYPE_PNG }
                | c<-colors]


{-
-- delete the bitmaps
delBitmaps :: [Bitmap ()] -> IO ()
delBitmaps bmps = sequence_ [bitmapDelete b | b<-bmps]
-}


-- update the status field 
-- shows current player and checks for no available moves, i.e. game end
updateStatus sf g
    = do mp <- get g player 
         case mp of
           Nothing -> return ()
           Just p -> do b <- get g board 
                        let moves = valid b
                        case moves of
                          [] -> do set sf [text := show p ++ " loses (no moves available)"]
                                   set g [player := Nothing]
                          _  -> set sf [text := show p ++ " to play"]


-- redraw the game board
drawBoard g dc (Rect x y w h)
    = do b <- varGet (gameBoard g)
         rs<- drawStacks (gameBitmaps g) b dc x' y'
         varSet (drawnRects g) rs
         -- highlight the selection if active
         i <- varGet (gameSel g)
         when (i>=0) $ drawRect dc (rs!!i) [pen:=penColored red 2, 
                                            brush:=brushTransparent]
    where x'= x + 10
          y' = y + h`div`2

-- draw a list of tile stacks
-- returns the list of bounding boxes (rectangles)
drawStacks bmps [] dc x y 
    = return []
drawStacks bmps (s:ss) dc x y
    = do r<-drawStack bmps s dc x y
         rs<-drawStacks bmps ss dc (x+rectWidth r+dx) y
         return (r:rs)
    where dx = 8

-- draw a single stack of tiles
drawStack bmps tiles dc x y
    = do rs<-sequence [ do { drawBitmap dc b (pt x' y') False []
                           ; sz <- bitmapGetSize b
                           ; return (rect (pt x' y') sz)
                           }
                        | (c,x',y')<-zip3 (reverse tiles)
                                     [x,x+dx..] [y,y+dy..],
                        let b = bmps!!fromEnum c
                       ]
         return (rectUnions rs)
    where n = length tiles
          dx = 4
          dy = -8

                  

-- handle a mouse click and perform a human player move
humanPlay g sf pt
    = do mp <- get g player
         when (mp==Just Human) $
              do b <- get g board
                 i <- get g selection
                 rs <- varGet (drawnRects g)
                 let j = rectIndex rs pt
                 if i>=0 && j>=0 && (i,j)`elem`valid b then 
                     do set g [board := play b (i,j), 
                               player := Just Computer, 
                               selection := (-1)]
                        updateStatus sf g
                     else if i==j then 
                              set g [selection := (-1)]
                          else 
                              set g [selection := j]
                 repaint g

-- convert a screen position into an index or -1
rectIndex rs pt 
    = head ([i | (i,r)<-zip [0..] rs, rectContains r pt] ++ [-1])


{-
humanPlay vplayer vgame vsel updt Nothing 
    = return ()
humanPlay vplayer vgame vsel updt (Just j) 
    = do player <- varGet vplayer
         game <- varGet vgame
         sel <- varGet vsel
         case (player,sel) of
           (Human, Nothing) -> do {varSet vsel (Just j); updt}
           (Human, Just i) -> do varSet vsel Nothing 
                                 when ((i,j) `elem` valid game) $
                                     do varSet vgame (play game (i,j))
                                        varSet vplayer Computer
                                 updt
           _ -> return ()
-}



-- perform a computer opponent move
computerPlay g sf 
    = do p <- get g player
         when (p==Just Computer) $ 
              do b <- get g board
                 d <- get g level
                 case bestmove d b of
                         Nothing -> return ()
                         Just m -> do set g [board:= play b m, player:= Just Human]
                                      updateStatus sf g



{-
-- perform a computer opponent move
computerPlay vplayer vgame vlevel updt
    = do player <- varGet vplayer
         game <- varGet vgame
         level <- varGet vlevel
         case player of
           Human -> return ()
           Computer -> case bestmove' (depth level) game of
                         (_, Nothing) -> return ()
                         (e, Just m) -> 
                             do varSet vgame (play game m)
                                varSet vplayer Human
                                updt
-}
 
-- show a move as a string
showMove :: Player -> Board -> Move -> String
showMove p b (i,j) = show p ++ ": " ++ show (b!!i) ++ " to " ++ show (b!!j)