packages feed

Ninjas-0.1.0.0: src/Server.hs

{-# LANGUAGE RecordWildCards #-}
module Server (ServerEnv(..), defaultServerEnv, serverMain) where

import Control.Applicative (Applicative)
import Control.Monad (when, guard)
import Data.Foldable (for_)
import Data.IntMap (IntMap)
import Data.List (intercalate, sortBy, (\\))
import Data.Maybe (fromMaybe)
import Data.Ord (comparing)
import Data.Traversable (sequenceA)
import Graphics.Gloss.Data.Point
import Graphics.Gloss.Geometry.Line
import Network (PortID(..))
import qualified Data.IntMap as IntMap

import Simulation
import Character
import NetworkMessages
import Parameters

import NetworkedGame.Server (NetworkServer(..), announce, announceOne, Handles)
import NetworkedGame.Handles (ConnectionId(..))
import qualified NetworkedGame.Server as NS

data ServerEnv = ServerEnv
  { npcCount          :: Int
  , initialSmokebombs :: Int
  , serverPort        :: Int
  }

data ServerWorld = ServerWorld
  { serverNpcs    :: IntMap Character
  , serverPlayers :: IntMap Player
  , serverMode    :: ServerMode
  , serverLobby   :: [(Int,String)]
  }

defaultServerEnv :: ServerEnv
defaultServerEnv = ServerEnv
  { npcCount          = 10
  , initialSmokebombs = 1
  , serverPort        = 16000
  }

-- | Main entry point for server
serverMain :: ServerEnv -> IO ()
serverMain env =
  do w                  <- initServerWorld env []
     let ServerEnv { serverPort = port } = env
     let settings = NetworkServer
           { serverPort = PortNumber $ fromIntegral port
           , eventsPerSecond = Parameters.eventsPerSecond
           , onTick     = updateServerWorld
           , onConnect  = connect
           , onDisconnect = disconnect
           , onCommand = command env
           }
     NS.serverMain settings w

-----------------------------------------------------------------------
-- Game logic
-----------------------------------------------------------------------

readyCountdown :: Handles -> ServerWorld -> IO ServerWorld
readyCountdown hs w =
  do announce hs $ ServerMessage "3"
     return w { serverMode = Starting 0 }

-- | Construct a new game world preserving the scores from
-- the previous world and adding the lobby players in.
newGame :: ServerEnv -> ServerWorld -> IO ServerWorld
newGame env w =
  do let scores = serverScores w ++ [(i,u,0) | (i,u) <- serverLobby w]
     initServerWorld env scores

generateSetWorld :: ServerWorld -> ServerCommand
generateSetWorld w =
  SetWorld [(i, charPos char, charFacing char) | (i,char) <- allCharacters w]

allCharacters :: ServerWorld -> [(Int,Character)]
allCharacters w = IntMap.toList (fmap playerCharacter (serverPlayers w))
               ++ IntMap.toList (serverNpcs w)

updateWorldForCommand ::
  ServerEnv ->
  Int {- ^ ID of sender -} ->
  Handles ->
  ServerWorld ->
  ClientCommand ->
  IO ServerWorld
updateWorldForCommand env i hs w msg =
  do let Just me = IntMap.lookup i $ serverPlayers w
         myPos = charPos $ playerCharacter me
         mapPlayer f = w { serverPlayers =
                           IntMap.update (Just . f) i (serverPlayers w) }
         mapMyNpc = mapPlayer . mapPlayerCharacter

     case msg of
       NewGame | serverMode w == Stopped
                 || not (isInLobby i w)
                    && IntMap.size (serverPlayers w) == 1 ->
                        do w' <- newGame env w
                           announce hs $ generateSetWorld w'
                           readyCountdown hs w'
               | otherwise -> return w

       _       | serverMode w /= Playing || isStuckPlayer me -> return w

       ClientSmoke
         | hasSmokebombs me ->
           do announce hs $ ServerSmoke $ charPos $ playerCharacter me
              return $ mapPlayer consumeSmokebomb

       ClientCommand cmd ->
         case cmd of
           Move _ pos0
                -- Disregard where the player says he is moving from
             | pointInBox pos boardMin boardMax ->
               do announce hs $ ServerCommand i
                    $ Move myPos pos
                  return $ mapMyNpc $ walkingCharacter pos
             where
             pos = constrainPoint myPos pos0

           Stop     ->
               do announce hs $ ServerCommand i cmd
                  return $ mapMyNpc $ waitingCharacter Nothing False

           Attack   ->
               do performAttack hs i w
           _        -> return w
       _          -> return w

serverScores :: ServerWorld -> [(Int,String,Int)]
serverScores w = [ (i, playerUsername p, playerScore p)
                 | (i,p) <- IntMap.toList $ serverPlayers w ]

initServerWorld :: ServerEnv -> [(Int,String,Int)] -> IO ServerWorld
initServerWorld env scores =
  do let npcIds      = take (npcCount env)
                     $ [0..] \\ [i | (i,_,_) <- scores]
     let newPlayer   = initPlayer (initialSmokebombs env)
         serverMode  = Stopped
         serverLobby = []
     serverPlayers   <- fmap IntMap.fromList
                      $ mapM (\(i,u,s) -> fmap ((,) i) (newPlayer u s)) scores
     serverNpcs      <- fmap IntMap.fromList
                      $ mapM (\i -> fmap ((,) i) (initServerCharacter True)) npcIds
     return ServerWorld { .. }

startingMode :: Handles -> Float -> Float -> ServerWorld -> IO ServerWorld
startingMode hs t duration w =
  do when (boundary (1*0.6)) $ announce hs $ ServerMessage "2"
     when (boundary (2*0.6)) $ announce hs $ ServerMessage "1"
     when (boundary (3*0.6)) $ announce hs $ ServerMessage "GO!"
     mode' <- if boundary (4*0.6)
                then do announce hs ServerReady
                        return Playing
                else return $ Starting duration'
     return $ w { serverMode = mode' }
  where
  duration'	= duration + t
  boundary x 	= duration < x && x <= duration'

updateServerWorld    :: Handles -> Float -> ServerWorld -> IO ServerWorld
updateServerWorld hs t w =
  case serverMode w of
    Stopped -> return w
    Starting duration -> startingMode hs t duration w
    Playing ->
     do pcs'  <- mapWithKeyA (playerLogic hs t) $ serverPlayers w
        npcs' <- mapWithKeyA (characterLogic hs t True) $ serverNpcs w

        let winners = IntMap.filter isWinner pcs'

        pcs2 <- if IntMap.null winners
                then return pcs'
                else endGame hs winners pcs' "deception"

        let mode'
              | IntMap.null winners = Playing
              | otherwise          = Stopped

        return w { serverPlayers = pcs2
                 , serverNpcs    = npcs'
                 , serverMode    = mode'
                 }

endGame :: Handles -> IntMap Player -> IntMap Player -> String ->
  IO (IntMap Player)
endGame hs winners players reason =

  do -- Declare victory
     announce hs $ ServerMessage
       $ commas (map playerUsername (IntMap.elems winners))
          ++ " wins by " ++ reason ++ "!"

     -- Update scores
     let players' = IntMap.mapWithKey (addVictory (IntMap.keys winners)) players

     -- Announce scores
     announce hs
       $ ServerMessage $ commas $ map prettyScore
       $ reverse $ sortBy (comparing playerScore) $ IntMap.elems players'

     return players'

prettyScore :: Player -> String
prettyScore p = playerUsername p ++ ": " ++ show (playerScore p)

commas :: [String] -> String
commas = intercalate ", "

addVictory :: [Int] -> Int -> Player -> Player
addVictory winners name p
  | name `elem` winners = p { playerScore = 1 + playerScore p }
  | otherwise = p

playerLogic :: Handles -> Float -> Int -> Player -> IO Player
playerLogic hs t name p =
  do char <- characterLogic hs t False name $ playerCharacter p
     let p' = p { playerCharacter = char }
     case whichPillar (charPos char) of
       Just i | i `notElem` playerVisited p ->
         do announce hs ServerDing
            return p' { playerVisited = i : playerVisited p' }
       _ -> return p'

characterLogic :: Handles -> Float -> Bool -> Int -> Character -> IO Character
characterLogic hs t think charName char =
  do let (char',_,mbTask) = stepCharacter t char

     case guard think >> mbTask of

       Just ChooseWait ->
         do time <- pickWaitTime True
            return $ waitingCharacter time False char'

       Just ChooseDestination ->
         do tgt <- randomBoardPoint
            announce hs $ ServerCommand charName
                        $ Move (charPos char) tgt
            return $ walkingCharacter tgt char'

       Nothing -> return char'

constrainPoint :: Point -> Point -> Point
constrainPoint from
  = aux intersectSegVertLine (fst boardMin)
  . aux intersectSegVertLine (fst boardMax)
  . aux intersectSegHorzLine (snd boardMin)
  . aux intersectSegHorzLine (snd boardMax)
  where
  aux f x p = fromMaybe p (f from p x)

connect :: Handles -> ConnectionId -> ServerWorld -> IO ServerWorld
connect hs i w =
  do announceOne hs i $ generateSetWorld w
     return w

command ::
  ServerEnv ->
  Handles ->
  ConnectionId {- ^ player who sent command -} ->
  ClientCommand ->
  ServerWorld ->
  IO ServerWorld
command _   hs (ConnectionId i) (ClientJoin name) w = addClient hs i name w
command env hs (ConnectionId i) msg w
      -- The ids of lobby players might overlap with NPCs
    | isInLobby i w
        && msg /= NewGame = return w
    | otherwise = updateWorldForCommand env i hs w msg

disconnect :: Handles -> ConnectionId -> ServerWorld -> IO ServerWorld
disconnect hs (ConnectionId i) w =
  do putStrLn $ "Client disconnect with id " ++ show i
     case IntMap.lookup i $ serverPlayers w of
       Just p ->
         do let ps = IntMap.delete i $ serverPlayers w
            announce hs $ ServerCommand i Die
            announce hs $ ServerMessage $ playerUsername p ++ " disconnected"

            when (IntMap.null ps) $ announce hs $ ServerMessage "Game Over"

            let mode | IntMap.null ps = Stopped
                     | otherwise      = serverMode w

            return w { serverPlayers = ps
                     , serverMode    = mode
                     }

       Nothing ->
         case lookup i $ serverLobby w of
           Just u ->
            do announce hs $ ServerMessage $ u ++ " left lobby"
               return w { serverLobby = [(k,v) | (k,v) <- serverLobby w
                                                 , k /= i] }
           Nothing -> return w

addClient :: Handles -> Int -> String -> ServerWorld -> IO ServerWorld
addClient hs i name w =
  do for_ (serverLobby w) $ \(_,u) ->
         announceOne hs (ConnectionId i) $ ServerMessage $ u ++ " in lobby"
     for_ (serverPlayers w) $ \p ->
         announceOne hs (ConnectionId i) $ ServerMessage $ playerUsername p ++ " in game"
     announce hs $ ServerMessage $ name ++ " joined lobby"
     return $ w { serverLobby = (i,name) : serverLobby w }
 -- XXX : Check that the client isn't already registered

performAttack :: Handles -> Int -> ServerWorld -> IO ServerWorld
performAttack hs attackId w =
  do let Just attacker = IntMap.lookup attackId $ serverPlayers w
         them = IntMap.delete attackId $ serverPlayers w
     let attackChar = playerCharacter attacker

     announce hs $ ServerCommand attackId Attack

     let me' = mapPlayerCharacter attackingCharacter attacker
     them' <- mapWithKeyA (attackPlayer hs attacker) them
     npcs' <- mapWithKeyA (attackCharacter hs attackChar) (serverNpcs w)

     let winningAttack = all isDeadPlayer $ IntMap.elems them'
     everyone <- if winningAttack
                   then endGame hs (IntMap.singleton attackId me')
                                   (IntMap.insert attackId me' them') "force"
                   else return $ IntMap.insert attackId me' them'

     let mode
           | winningAttack = Stopped
           | otherwise     = Playing

     return $ w { serverPlayers = everyone
                , serverNpcs    = npcs'
                , serverMode    = mode
                }

attackCharacter :: Handles -> Character -> Int -> Character -> IO Character
attackCharacter hs attacker targetId target
  | canHitPoint attacker (charPos target) =
     do announce hs $ ServerCommand targetId Stun
        return $ stunnedCharacter target
  | otherwise = return target

attackPlayer :: Handles -> Player -> Int -> Player -> IO Player
attackPlayer hs attacker charName target
  | canHitPoint attackerChar (charPos targetChar) =
     do let attackerName = playerUsername attacker
        announce hs $ ServerCommand charName Die
        announceOne hs (ConnectionId charName)
          $ ServerMessage $ "Killed by " ++ attackerName
        return $ mapPlayerCharacter deadCharacter target
  | otherwise = return target
  where
  targetChar = playerCharacter target
  attackerChar = playerCharacter attacker

mapWithKeyA :: Applicative f => (Int -> a -> f b) -> IntMap a -> f (IntMap b)
mapWithKeyA f m = sequenceA $ IntMap.mapWithKey f m

-----------------------------------------------------------------------
-- Player predicates
-----------------------------------------------------------------------

isInLobby :: Int -> ServerWorld -> Bool
isInLobby i w = i `elem` map fst (serverLobby w)

isStuckPlayer :: Player -> Bool
isStuckPlayer p =
  case charState (playerCharacter p) of
    Dead          -> True
    Attacking {}  -> True
    _             -> False

isDeadPlayer :: Player -> Bool
isDeadPlayer p = Dead == charState (playerCharacter p)

isWinner :: Player -> Bool
isWinner p = length (playerVisited p) == length pillars