Sophie

Sophie

distrib > Fedora > 15 > i386 > by-pkgid > f81276b29f7c9661cdbaed2e97fbf6c3 > files > 31

ghc-glade-devel-0.12.0-4.fc15.i686.rpm

-- Copyright (c) 2006, Wouter Swierstra
-- All rights reserved.
-- This code is released under the BSD license
--   included in this distribution

-- Imports

import IO
import Maybe
import List
import Graphics.UI.Gtk hiding (Cross)
import Data.IORef
import Control.Monad

-- Players, boards and some useful pure functions

data Player =  Nought | Blank | Cross deriving (Ord, Eq, Show)

next        :: Player -> Player
next Nought =  Cross
next Blank  =  Blank
next Cross  =  Nought

type Board =  [[Player]]

size :: Int
size =  3

empty :: Board
empty =  replicate size (replicate size Blank)

move       :: Int -> Player -> Board -> Maybe Board
move n p b = case y of
               Blank     -> Just (chop size (xs ++ (p : ys)))
               _         -> Nothing
               where
                 (xs,y:ys) = splitAt n (concat b)

chop      :: Int -> [a] -> [[a]]
chop n [] =  []
chop n xs =  take n xs : chop n (drop n xs)

diag     :: [[a]] -> [a]
diag xss =  [xss !! n !! n | n <- [0 .. length xss - 1]]

full   :: Board -> Bool
full b =  all (all (/= Blank)) b

wins     :: Player -> Board -> Bool
wins p b =  any (all (== p)) b
            || any (all (== p)) (transpose b)
            || all (== p) (diag b)
            || all (== p) (diag (reverse b))

won   :: Board -> Bool
won b =  wins Nought b || wins Cross b

-- The state and GUI

data State = State {
  board :: Board,
  turn :: Player
}

data GUI = GUI {
  disableBoard :: IO (),
  resetBoard :: IO (),
  setSquare :: Int -> Player -> IO (),
  setStatus :: String -> IO ()
}

-- reset the game
reset gui (State board turn) = do
  setStatus gui "Player Cross: make your move."
  resetBoard gui
  return (State empty Cross)

-- when a square is clicked on, try to make a move.
--   if the square is already occupied, nothing happens
--   otherwise, update the board, let the next player make his move,
--     and check whether someone has won or the board is full.
occupy gui square st@(State board player) = do
  case move square player board of
    Nothing -> return st
    Just newBoard -> do
      setSquare gui square player
      handleMove gui newBoard player
      return (State newBoard (next player))

-- check whether a board is won or full
handleMove gui board player
  | wins player board = do
      setStatus gui ("Player "  ++ show player ++ " wins!")
      disableBoard gui
  | full board = do
      setStatus gui "It's a draw."
      disableBoard gui
  | otherwise = do
      setStatus gui ("Player " ++ show (next player) ++ ": make your move")

main = do
  initGUI
  window <- windowNew
  window `onDestroy` mainQuit
  set window [ windowTitle := "Noughty"
             , windowResizable := False ]
  label <- labelNew (Just "Player Cross: make your move.")
  vboxOuter <- vBoxNew False 0
  vboxInner <- vBoxNew False 5

-- Add an initial board to the inner vBox and make the menu bar
  (squares, images) <- addFieldsTo vboxInner
  (mb,newGame,quit) <- makeMenuBar

  -- Construct the GUI actions that abstracts from the actual widgets
  gui <- guiActions squares images label

  -- Initialize the state
  state <- newIORef State { board = empty, turn = Cross }
  let modifyState f = readIORef state >>= f >>= writeIORef state

  -- Add action handlers
  onActivateLeaf quit mainQuit
  onActivateLeaf newGame $ modifyState $ reset gui
  zipWithM_ (\square i ->
    onPressed square $ modifyState $ occupy gui i)
    squares [0..8]

  -- Assemble the bits
  set vboxOuter [ containerChild := mb
                , containerChild := vboxInner ]
  set vboxInner [ containerChild := label
                , containerBorderWidth := 10 ]
  set window [ containerChild := vboxOuter ]

  widgetShowAll window
  mainGUI

guiActions buttons images label = do
  noughtPic <- pixbufNewFromFile "Nought.png"
  crossPic  <- pixbufNewFromFile "Cross.png"
  return GUI {
    disableBoard = mapM_ (flip widgetSetSensitivity False) buttons,
    resetBoard = do
      mapM_ (\i -> imageClear i >> widgetQueueDraw i) images
      mapM_ (flip widgetSetSensitivity True) buttons,
    setSquare = \ i player ->
      case player of
        Cross -> set (images !! i) [ imagePixbuf := crossPic ]
        Nought-> set (images !! i) [ imagePixbuf := noughtPic ],
    setStatus = labelSetText label}

makeMenuBar = do
  mb <- menuBarNew
  fileMenu <- menuNew
  newGame <- menuItemNewWithMnemonic "_New Game"
  quit <- menuItemNewWithMnemonic "_Quit"
  file <- menuItemNewWithMnemonic "_Game"
  menuShellAppend fileMenu newGame
  menuShellAppend fileMenu quit
  menuItemSetSubmenu file fileMenu
  containerAdd mb file
  return (mb,newGame,quit)

addFieldsTo container = do
  table <- tableNew 5 5 False
  buttons@[b0,b1,b2,b3,b4,b5,b6,b7,b8] <- replicateM 9 squareNew
  images <- replicateM 9 imageNew
  zipWithM_ containerAdd buttons images
  tableAttachDefaults table b0 0 1 0 1
  tableAttachDefaults table b1 2 3 0 1
  tableAttachDefaults table b2 4 5 0 1
  tableAttachDefaults table b3 0 1 2 3
  tableAttachDefaults table b4 2 3 2 3
  tableAttachDefaults table b5 4 5 2 3
  tableAttachDefaults table b6 0 1 4 5
  tableAttachDefaults table b7 2 3 4 5
  tableAttachDefaults table b8 4 5 4 5
  vline1 <- vSeparatorNew
  vline2 <- vSeparatorNew
  hline1 <- hSeparatorNew
  hline2 <- hSeparatorNew
  tableAttachDefaults table vline1 1 2 0 5
  tableAttachDefaults table vline2 3 4 0 5
  tableAttachDefaults table hline1 0 5 1 2
  tableAttachDefaults table hline2 0 5 3 4
  tableSetRowSpacings table 0
  tableSetColSpacings table 0
  containerAdd container table
  return (buttons, images)

squareNew = do
  square <- buttonNew
  widgetSetSizeRequest square 100 100
  set square [ widgetCanFocus := False, buttonRelief := ReliefNone]
  return square