{-| Module : Game.Werewolf.Engine Description : Engine functions. Copyright : (c) Henry J. Wylde, 2015 License : BSD3 Maintainer : public@hjwylde.com Engine functions. -} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} module Game.Werewolf.Engine ( -- * Loop checkTurn, checkGameOver, -- * Game -- ** Manipulations startGame, killPlayer, -- ** Queries isSeersTurn, isVillagersTurn, isWerewolvesTurn, isGameOver, getPlayerSee, getPlayerVote, -- ** Reading and writing defaultFilePath, writeGame, readGame, deleteGame, doesGameExist, -- * Player -- ** Manipulations createPlayers, -- ** Queries doesPlayerExist, isPlayerSeer, isPlayerVillager, isPlayerWerewolf, isPlayerAlive, isPlayerDead, -- * Role randomiseRoles, ) where import Control.Lens hiding (cons) import Control.Monad.Except import Control.Monad.Random import Control.Monad.State hiding (state) import Control.Monad.Writer import Data.List.Extra import qualified Data.Map as Map import Data.Text (Text) import Game.Werewolf.Game hiding (isGameOver, isSeersTurn, isVillagersTurn, isWerewolvesTurn, killPlayer) import qualified Game.Werewolf.Game as Game import Game.Werewolf.Player hiding (doesPlayerExist) import qualified Game.Werewolf.Player as Player import Game.Werewolf.Response import Game.Werewolf.Role (Role, villagerRole, werewolfRole, _allegiance) import qualified Game.Werewolf.Role as Role import System.Directory import System.FilePath import System.Random.Shuffle checkTurn :: (MonadState Game m, MonadWriter [Message] m) => m () checkTurn = get >>= \game -> checkTurn' >> get >>= \game' -> unless (game == game') checkTurn checkTurn' :: (MonadState Game m, MonadWriter [Message] m) => m () checkTurn' = use turn >>= \turn' -> case turn' of NightFalling -> advanceTurn DayBreaking -> advanceTurn Seers -> do seersCount <- uses players (length . filterAlive . filterSeers) votes' <- use sees when (seersCount == Map.size votes') $ do forM_ (Map.toList votes') $ \(seerName, targetName) -> do target <- uses players (findByName_ targetName) tell [playerSeenMessage seerName target] advanceTurn Villagers -> do playersCount <- uses players (length . filterAlive) votes' <- use votes when (playersCount == Map.size votes') $ do tell $ map (uncurry playerMadeLynchVoteMessage) (Map.toList votes') case last $ groupSortOn (length . flip elemIndices (Map.elems votes')) (nub $ Map.elems votes') of [lynchedName] -> do target <- uses players (findByName_ lynchedName) killPlayer target tell [playerLynchedMessage (target ^. name) (target ^. role . Role.name)] _ -> uses players (filterAlive . filterScapegoats) >>= \aliveScapegoats -> case aliveScapegoats of (scapegoat:_) -> killPlayer scapegoat >> tell [scapegoatLynchedMessage (scapegoat ^. name)] [] -> tell [noPlayerLynchedMessage] advanceTurn Werewolves -> do aliveWerewolves <- uses players (filterAlive . filterWerewolves) votes' <- use votes when (length aliveWerewolves == Map.size votes') $ do advanceTurn case last $ groupSortOn (length . flip elemIndices (Map.elems votes')) (nub $ Map.elems votes') of [targetName] -> do target <- uses players (findByName_ targetName) killPlayer target tell [playerDevouredMessage (target ^. name) (target ^. role . Role.name), villagersLynchVoteMessage] _ -> tell [noPlayerDevouredMessage, villagersLynchVoteMessage] NoOne -> return () advanceTurn :: (MonadState Game m, MonadWriter [Message] m) => m () advanceTurn = do turn' <- use turn alivePlayers <- uses players filterAlive let nextTurn = if length (nub $ map (_allegiance . _role) alivePlayers) <= 1 then NoOne else head $ filter (turnAvailable $ map _role alivePlayers) (drop1 $ dropWhile (turn' /=) turnRotation) tell $ turnMessages nextTurn alivePlayers turn .= nextTurn sees .= Map.empty votes .= Map.empty checkGameOver :: (MonadState Game m, MonadWriter [Message] m) => m () checkGameOver = do aliveAllegiances <- uses players $ nub . map (_allegiance . _role) . filterAlive when (length aliveAllegiances <= 1) $ turn .= NoOne >> get >>= tell . gameOverMessages startGame :: (MonadError [Message] m, MonadWriter [Message] m) => Text -> [Player] -> m Game startGame callerName players = do when (playerNames /= nub playerNames) $ throwError [privateMessage [callerName] "Player names must be unique."] when (length players < 7) $ throwError [privateMessage [callerName] "Must have at least 7 players."] when (length players > 24) $ throwError [privateMessage [callerName] "Cannot have more than 24 players."] let game = newGame players tell $ newGameMessages game return game where playerNames = map _name players killPlayer :: MonadState Game m => Player -> m () killPlayer player = players %= map (\player' -> if player' == player then player' & state .~ Dead else player') isSeersTurn :: MonadState Game m => m Bool isSeersTurn = gets Game.isSeersTurn isVillagersTurn :: MonadState Game m => m Bool isVillagersTurn = gets Game.isVillagersTurn isWerewolvesTurn :: MonadState Game m => m Bool isWerewolvesTurn = gets Game.isWerewolvesTurn isGameOver :: MonadState Game m => m Bool isGameOver = gets Game.isGameOver getPlayerSee :: MonadState Game m => Text -> m (Maybe Text) getPlayerSee playerName = use $ sees . at playerName getPlayerVote :: MonadState Game m => Text -> m (Maybe Text) getPlayerVote playerName = use $ votes . at playerName defaultFilePath :: MonadIO m => m FilePath defaultFilePath = ( defaultFileName) <$> liftIO getHomeDirectory defaultFileName :: FilePath defaultFileName = ".werewolf" readGame :: MonadIO m => m Game readGame = liftIO . fmap read $ defaultFilePath >>= readFile writeGame :: MonadIO m => Game -> m () writeGame game = liftIO $ defaultFilePath >>= flip writeFile (show game) deleteGame :: MonadIO m => m () deleteGame = liftIO $ defaultFilePath >>= removeFile doesGameExist :: MonadIO m => m Bool doesGameExist = liftIO $ defaultFilePath >>= doesFileExist createPlayers :: MonadIO m => [Text] -> [Role] -> m [Player] createPlayers playerNames extraRoles = zipWith newPlayer playerNames <$> randomiseRoles extraRoles (length playerNames) doesPlayerExist :: MonadState Game m => Text -> m Bool doesPlayerExist name = uses players $ Player.doesPlayerExist name isPlayerSeer :: MonadState Game m => Text -> m Bool isPlayerSeer name = uses players $ isSeer . findByName_ name isPlayerVillager :: MonadState Game m => Text -> m Bool isPlayerVillager name = uses players $ isVillager . findByName_ name isPlayerWerewolf :: MonadState Game m => Text -> m Bool isPlayerWerewolf name = uses players $ isWerewolf . findByName_ name isPlayerAlive :: MonadState Game m => Text -> m Bool isPlayerAlive name = uses players $ isAlive . findByName_ name isPlayerDead :: MonadState Game m => Text -> m Bool isPlayerDead name = uses players $ isDead . findByName_ name randomiseRoles :: MonadIO m => [Role] -> Int -> m [Role] randomiseRoles extraRoles n = liftIO . evalRandIO . shuffleM $ extraRoles ++ werewolfRoles ++ villagerRoles where extraWerewolfRoles = filter ((==) Role.Werewolves . _allegiance) extraRoles extraVillagerRoles = filter ((==) Role.Villagers . _allegiance) extraRoles werewolfRoles = replicate (n `quot` 6 + 1 - length extraWerewolfRoles) werewolfRole villagerRoles = replicate (n - length (extraVillagerRoles ++ werewolfRoles)) villagerRole