packages feed

Nomyx-0.0.1: src/Multi.hs

{-# LANGUAGE DeriveDataTypeable, FlexibleContexts, GeneralizedNewtypeDeriving,
    MultiParamTypeClasses, TemplateHaskell, TypeFamilies, TypeOperators,
    TypeSynonymInstances, FlexibleInstances, GADTs #-}

-- | This module manages multi-player games and commands.
module Multi where
--Multi, PlayerPassword, GetMulti(..), FindPlayer(..), PlayerMulti(..), GetNewPlayerNumber(..), NewPlayerU(..),
--    listGame, newGame, joinGame, leaveGame, subscribeGame, unsubscribeGame, showSubscribtion, showSubGame, newPlayer,
--    submitRule, myCatch, submitRuleI, showConstitution, showAllRules, listPlayers, amendConstitution, showPendingActions,
--    showMyPendingActions, doActionsI, doAction', showCompletedActions, quit, getPendingActions, doAction, games, getPlayersGame,
--    getPlayersName) where


import Prelude hiding (catch)
import Data.List
import Control.Monad.State
import Game
import Utils
import Interpret
import Data.Typeable
import Data.Function (on)
import Debug.Trace.Helpers()
import Language.Nomyx.Expression
import Data.Time
import Language.Haskell.Interpreter.Server
import Language.Nomyx.Evaluation
import Control.Concurrent.STM
import Data.Maybe
import Control.Concurrent


type PlayerPassword = String

data PlayerMulti = PlayerMulti   { mPlayerNumber :: PlayerNumber,
                                   mPlayerName :: PlayerName,
                                   mPassword :: PlayerPassword,
                                   inGame :: Maybe GameName}
                                   deriving (Eq, Show, Typeable)

--- | A structure to hold the active games and players
data Multi = Multi { games   :: [Game],
                     mPlayers :: [PlayerMulti]}
                     deriving (Eq, Typeable)

instance Show Multi where
   show Multi{games=gs, mPlayers=mps} = show (sort gs) ++ "\n" ++ show (sort mps)

defaultMulti :: Multi
defaultMulti = Multi [] []

-- | helper function to change a player's ingame status.
mayJoinGame :: Maybe GameName -> PlayerNumber -> [PlayerMulti] -> [PlayerMulti]
mayJoinGame maybename pn pl = case find (\(PlayerMulti mypn _ _ _) -> mypn == pn) pl of
                     Just o -> replace o o{ inGame = maybename} pl
                     Nothing -> pl

newPlayerU :: PlayerMulti -> StateT Multi IO ()
newPlayerU pm = do
   pms <- gets mPlayers
   modify (\multi -> multi { mPlayers = pm : pms})

findPlayer :: PlayerName -> StateT Multi IO (Maybe PlayerMulti)
findPlayer name =  fmap (find (\PlayerMulti {mPlayerName = pn} -> pn==name)) (gets mPlayers)

getNewPlayerNumber :: StateT Multi IO PlayerNumber
getNewPlayerNumber = do
   ps <- gets mPlayers
   return $ length ps + 1


addNewGame :: Game -> StateT Multi IO ()
addNewGame new = modify (\multi@Multi {games=gs} -> multi {games =  new:gs})

getGameByName :: GameName -> StateT Multi IO (Maybe Game)
getGameByName gn =  fmap (find (\(Game {gameName = n}) -> n==gn)) (gets games)

joinGamePlayer :: PlayerNumber -> GameName -> StateT Multi IO ()
joinGamePlayer pn game = modify (\multi -> multi {mPlayers = mayJoinGame (Just game) pn (mPlayers multi)})

leaveGameU :: PlayerNumber -> StateT Multi IO ()
leaveGameU pn = modify (\multi -> multi {mPlayers = mayJoinGame Nothing pn (mPlayers multi)})

-- | list the active games
listGame :: PlayerNumber -> StateT Multi IO ()
listGame _ = do
   gs <- gets games
   case length gs of
      0 -> say "No active games"
      _ -> do
         say "Active games:"
         say $ concatMap (\g -> gameName g ++ "\n") gs

-- | starts a new game
newGame :: String -> PlayerNumber -> StateT Multi IO ()
newGame name _ = do
   gs <- gets games
   case null $ filter (\p -> gameName p == name) gs of
      True -> do
         say $ "Creating a new game of name: " ++ name
         t <- liftIO getCurrentTime
         -- create a game with zero players
         modify (\m -> m {games = (initialGame name t):gs})
      False -> say $ "this name is already used"

uniqueGame :: String -> [Game] -> Bool
uniqueGame s gs = null $ filter (\p -> gameName p == s) gs

-- | join a game.
joinGame :: GameName -> PlayerNumber -> StateT Multi IO ()
joinGame game pn = do
   mg <- getGameByName game
   case mg of
      Nothing -> say $ "No game by that name"
      Just g -> do
         say "subscribing first."
         subscribeGame (gameName g) pn
         say $ "Joining game: " ++ game
         joinGamePlayer pn game


-- | leave a game (you remain subscribed).
leaveGame :: PlayerNumber -> StateT Multi IO  ()
leaveGame pn = do
   leaveGameU pn
   say "You left the game (you remain subscribed)."


-- | subcribe to a game.
subscribeGame :: GameName -> PlayerNumber -> StateT Multi IO ()
subscribeGame game pn = do
   m <- get
   inGameDo game $ do
      g <- get
      case find (\(PlayerInfo  { playerNumber=mypn}) -> mypn == pn ) (players g) of
         Just _ -> say "Already subscribed!"
         Nothing -> do
            say $ "Subscribing to game: " ++ game
            put g {players = PlayerInfo { playerNumber = pn,
                                          playerName = getPlayersName pn m} : (players g)}


-- | subcribe to a game.
unsubscribeGame :: GameName -> PlayerNumber -> StateT Multi IO ()
unsubscribeGame game pn = inGameDo game $ do
   g <- get
   case find (\(PlayerInfo  { playerNumber=mypn}) -> mypn == pn ) (players g) of
      Nothing -> say "Not subscribed!"
      Just _ -> do
         say $ "Unsubscribing to game: " ++ game
         put g {players = filter (\PlayerInfo { playerNumber = mypn} -> mypn /= pn) (players g)}


showSubGame :: GameName -> PlayerNumber -> StateT Multi IO  ()
showSubGame g _ = inGameDo g $ do
   ps <- gets players
   say $ concatMap show ps

showSubscribtion :: PlayerNumber -> StateT Multi IO  ()
showSubscribtion pn = inPlayersGameDo pn $ do
   ps <- gets players
   say $ concatMap show ps


-- | insert a rule in pending rules.
submitRule :: String -> String -> String -> PlayerNumber -> ServerHandle -> StateT Multi IO  ()
submitRule name text rule pn sh = inPlayersGameDo pn $ do
   --input the new rule (may fail if ill-formed)
   rs <- gets rules
   let rn = getFreeNumber $ map rNumber rs
   mnr <- enterRule rn name text rule pn sh
   case mnr of
      Just nr -> do
         r <- liftT $ evProposeRule nr
         if r == True then say $ "Your rule has been added to pending rules."
             else say $ "Error: Rule could not be proposed"
         return ()
      Nothing -> say $ "Please try again."

inputChoiceResult :: EventNumber -> Int -> PlayerNumber -> StateT Multi IO  ()
inputChoiceResult eventNumber choiceIndex pn = inPlayersGameDo pn $ liftT $ triggerChoice eventNumber choiceIndex

inputStringResult :: Event InputString -> String -> PlayerNumber -> StateT Multi IO  ()
inputStringResult event input pn = inPlayersGameDo pn $ liftT $ triggerEvent event (InputStringData input)

inputUpload :: PlayerNumber -> FilePath -> String -> ServerHandle -> StateT Multi IO  ()
inputUpload pn dir mod sh = inPlayersGameDo pn $ do
    m <- lift $ loadModule dir mod sh
    case m of
      Right _ -> do
         output pn $ "File loaded: " ++ show dir ++ " Module " ++ show mod ++"\n"
         return ()
      Left e -> do
         output pn $ "Compiler error: " ++ show e ++ "\n"
         return ()

output :: PlayerNumber -> String -> StateT Game IO ()
output pn s = modify (\game -> game { outputs = (pn, s) : (outputs game)})

-- | finds the corresponding game in the multistate and replaces it.
modifyGame :: Game -> StateT Multi IO  ()
modifyGame g = do
   Multi gs ps <- get
   case find (\myg -> gameName g == gameName myg) gs of
      Nothing -> error "modifyGame: No game by that name"
      Just oldg -> do
         let newgs = replace oldg g gs
         put $ Multi newgs ps

-- | reads a rule.
enterRule :: RuleNumber -> String -> String -> String -> PlayerNumber -> ServerHandle -> StateT Game IO (Maybe Rule)
enterRule num name text ruleText pn sh = do
   mrr <- lift $ interpretRule ruleText sh
   case mrr of
      Right ruleFunc -> return $ Just Rule {rNumber = num,
                      rName = name,
                      rDescription = text,
                      rProposedBy = pn,
                      rRuleCode = ruleText,
                      rRuleFunc = ruleFunc,
                      rStatus = Pending,
                      rAssessedBy = Nothing}
      Left e -> do
         output pn $ "Compiler error: " ++ show e ++ "\n"
         return Nothing


-- | show the constitution.
showConstitution :: PlayerNumber -> StateT Multi IO ()
showConstitution pn = inPlayersGameDo pn $ get >>= (say  .  show  .  activeRules)


-- | show every rules (including pendings and deleted)
showAllRules :: PlayerNumber -> StateT Multi IO ()	
showAllRules pn = inPlayersGameDo pn $ get >>= (say . show . rules)

displayPlayer :: PlayerMulti -> String
displayPlayer (PlayerMulti pn name _ (Just game)) = show pn ++ ": " ++ name ++ " in game: " ++ game ++ "\n"
displayPlayer (PlayerMulti pn name _ Nothing)     = show pn ++ ": " ++ name ++ "\n"


-- | quit the game
quit :: PlayerNumber -> IO ()
quit _ = putStrLn "quit"

-- | Utility functions

-- | replace the player's name in the list
setName :: String -> PlayerNumber -> [PlayerMulti] -> [PlayerMulti]
setName name pn pl = case find (\(PlayerMulti h _ _ _) -> h == pn) pl of
                        Just o -> replace o o{ mPlayerName = name} pl
                        Nothing -> pl


-- | returns the game the player is in						
getPlayersGame :: PlayerNumber -> Multi -> Maybe Game
getPlayersGame pn multi = do
        pi <- find (\(PlayerMulti n _ _ _) -> n==pn) (mPlayers multi)
        gn <- inGame pi
        find (\(Game {gameName=name}) -> name==gn) (games multi)

getPlayersName :: PlayerNumber -> Multi -> PlayerName
getPlayersName pn multi = do
   case find (\(PlayerMulti n _ _ _) -> n==pn) (mPlayers multi) of
      Nothing -> error "getPlayersName: No player by that number"
      Just pm -> mPlayerName pm

getPlayersName' :: Game -> PlayerNumber -> PlayerName
getPlayersName' g pn = do
   case find (\(PlayerInfo n _) -> n==pn) (players g) of
      Nothing -> error "getPlayersName: No player by that number in that game"
      Just pm -> playerName pm

-- | this function apply the given game actions to the game the player is in.
inPlayersGameDo :: PlayerNumber -> StateT Game IO () -> StateT Multi IO ()
inPlayersGameDo pn action = do
   multi <- get
   let mg = getPlayersGame pn multi
   case mg of
      Nothing -> say "You must be in a game"
      Just g -> do
         myg <- lift $ execWithGame action g
         modifyGame myg

inGameDo :: GameName -> StateT Game IO () -> StateT Multi IO ()
inGameDo game action = do
   gs <- gets games
   case find (\(Game {gameName =n}) -> n==game) gs of
      Nothing -> say "No game by that name"
      Just g -> do
         myg <- lift $ execWithGame action g
         modifyGame myg

instance Ord PlayerMulti where
  (<=) = (<=) `on` mPlayerNumber


triggerTimeEvent :: TVar Multi -> UTCTime -> IO()
triggerTimeEvent tm t = do
    m <- atomically $ readTVar tm
    gs' <- mapM (trig t) (games m)
    let m' = m {games = gs'}
    atomically $ writeTVar tm m'
       where trig t g =  execWithGame (liftT $ evTriggerTime t) g

execGame :: State Game () -> Game -> Game
execGame s g = execState s g

-- | get all events within time and time + 2 second
getTimeEvents :: UTCTime -> TVar Multi -> IO([UTCTime])
getTimeEvents time tm = do
    m <- atomically $ readTVar tm
    let times = catMaybes $ map getTimes $ concatMap events $ games m
    return $ filter (\t-> t >= time && t < addUTCTime 2 time) times

--TODO: fix this
launchTimeEvents :: TVar Multi -> IO()
launchTimeEvents tm = do
    now <- getCurrentTime
    --putStrLn $ "tick " ++ (show now)
    schedule <- getTimeEvents now tm
    when (length schedule /= 0) $ putStrLn "found time event"
    mapM_ (triggerTimeEvent tm) schedule
    after <- getCurrentTime
    --sleep 1 second minus rough delay of execution
    threadDelay $ truncate(1000000 - 1000000*(diffUTCTime after now))
    launchTimeEvents tm


getTimes :: EventHandler -> Maybe UTCTime
getTimes (EH _ _ (Time t) _) = Just t
getTimes _ = Nothing