werewolf-1.2.0.0: src/Game/Werewolf/Game.hs
{-|
Module : Game.Werewolf.Game
Description : Simplistic game data structure with lenses.
Copyright : (c) Henry J. Wylde, 2016
License : BSD3
Maintainer : public@hjwylde.com
A game is not quite as simple as players! Roughly speaking though, this engine is /stateful/. The
game state only changes when a /command/ is issued. Thus, this module defines the 'Game' data
structure and any fields required to keep track of the current state.
-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE TemplateHaskell #-}
module Game.Werewolf.Game (
-- * Game
Game,
variant, stage, round, players, boots, allowedVoters, divine, fallenAngelLynched, healUsed,
hunterRetaliated, jesterRevealed, passed, poison, poisonUsed, priorProtect, protect, roleModel,
scapegoatBlamed, see, votes,
Variant(..),
_Standard, _NoRoleKnowledge,
Stage(..),
_DruidsTurn, _GameOver, _HuntersTurn1, _HuntersTurn2, _Lynching, _OraclesTurn, _OrphansTurn,
_ProtectorsTurn, _ScapegoatsTurn, _SeersTurn, _Sunrise, _Sunset, _VillageDrunksTurn,
_VillagesTurn, _WerewolvesTurn, _WitchsTurn,
allStages,
stageCycle, stageAvailable,
newGame,
-- ** Folds
votee,
-- ** Prisms
firstRound, secondRound, thirdRound,
-- ** Searches
getAllowedVoters, getPendingVoters,
-- ** Queries
hasAnyoneWon, hasFallenAngelWon, hasVillagersWon, hasWerewolvesWon,
) where
import Control.Lens hiding (isn't)
import Control.Lens.Extra
import Data.List.Extra
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Maybe
import Data.String.Humanise
import Data.Text (Text)
import Game.Werewolf.Player
import Prelude hiding (round)
-- | There are a few key pieces of information that a game always needs to hold. These are:
--
-- * the 'stage',
-- * the 'round' number and
-- * the 'players'.
--
-- Any further fields on the game are specific to one or more roles (and their respective turns!).
-- Some of the additional fields are reset each round (e.g., the Seer's 'see') while others are
-- kept around for the whole game (e.g., the Orphan's 'roleModel').
data Game = Game
{ _variant :: Variant
, _stage :: Stage
, _round :: Int
, _players :: [Player]
, _boots :: Map Text [Text]
, _allowedVoters :: [Text] -- ^ Jester, Scapegoat
, _divine :: Maybe Text -- ^ Oracle
, _fallenAngelLynched :: Bool -- ^ Fallen Angel
, _healUsed :: Bool -- ^ Witch
, _hunterRetaliated :: Bool -- ^ Hunter
, _jesterRevealed :: Bool -- ^ Jester
, _passed :: Bool -- ^ Witch
, _poison :: Maybe Text -- ^ Witch
, _poisonUsed :: Bool -- ^ Witch
, _priorProtect :: Maybe Text -- ^ Protector
, _protect :: Maybe Text -- ^ Protector
, _roleModel :: Maybe Text -- ^ Orphan
, _scapegoatBlamed :: Bool -- ^ Scapegoat
, _see :: Maybe Text -- ^ Seer
, _votes :: Map Text Text -- ^ Villagers and Werewolves
} deriving (Eq, Read, Show)
data Variant = Standard | NoRoleKnowledge
deriving (Eq, Read, Show)
instance Humanise Variant where
humanise Standard = "standard"
humanise NoRoleKnowledge = "no role knowledge"
-- | Most of these are fairly self-explainable (the turn stages). 'Sunrise' and 'Sunset' are
-- provided as meaningful breaks between the day and night as, for example, a 'VillagesTurn' may
-- not always be available (curse that retched Scapegoat).
--
-- Once the game reaches a turn stage, it requires a /command/ to help push it past. Often only
-- certain roles and commands may be performed at any given stage.
data Stage = DruidsTurn | GameOver | HuntersTurn1 | HuntersTurn2 | Lynching | OraclesTurn
| OrphansTurn | ProtectorsTurn | ScapegoatsTurn | SeersTurn | Sunrise | Sunset
| VillageDrunksTurn | VillagesTurn | WerewolvesTurn | WitchsTurn
deriving (Eq, Read, Show)
instance Humanise Stage where
humanise DruidsTurn = "Druid's turn"
humanise GameOver = "Game over"
humanise HuntersTurn1 = "Hunter's turn"
humanise HuntersTurn2 = "Hunter's turn"
humanise Lynching = "Lynching"
humanise OraclesTurn = "Oracle's turn"
humanise OrphansTurn = "Orphan's turn"
humanise ProtectorsTurn = "Protector's turn"
humanise ScapegoatsTurn = "Scapegoat's turn"
humanise SeersTurn = "Seer's turn"
humanise Sunrise = "Sunrise"
humanise Sunset = "Sunset"
humanise VillageDrunksTurn = "Village Drunk's turn"
humanise VillagesTurn = "village's turn"
humanise WerewolvesTurn = "Werewolves' turn"
humanise WitchsTurn = "Witch's turn"
makeLenses ''Game
makePrisms ''Variant
makePrisms ''Stage
-- | All of the 'Stage's in the order that they should occur.
allStages :: [Stage]
allStages =
[ Sunset
, OrphansTurn
, VillageDrunksTurn
, SeersTurn
, OraclesTurn
, ProtectorsTurn
, WerewolvesTurn
, WitchsTurn
, Sunrise
, HuntersTurn2
, DruidsTurn
, VillagesTurn
, Lynching
, HuntersTurn1
, ScapegoatsTurn
, GameOver
]
-- | An infinite cycle of all 'Stage's in the order that they should occur.
stageCycle :: [Stage]
stageCycle = cycle allStages
-- | Checks whether the stage is available for the given 'Game'. Most often this just involves
-- checking if there is an applicable role alive, but sometimes it is more complex.
--
-- One of the more complex checks here is for the 'VillagesTurn'. If the Fallen Angel is in play,
-- then the 'VillagesTurn' is available on the first day rather than only after the first night.
stageAvailable :: Game -> Stage -> Bool
stageAvailable game DruidsTurn = has (players . druids . alive) game
stageAvailable _ GameOver = False
stageAvailable game HuntersTurn1 =
has (players . hunters . dead) game
&& not (game ^. hunterRetaliated)
stageAvailable game HuntersTurn2 =
has (players . hunters . dead) game
&& not (game ^. hunterRetaliated)
stageAvailable game Lynching = Map.size (game ^. votes) > 0
stageAvailable game OraclesTurn = has (players . oracles . alive) game
stageAvailable game OrphansTurn =
has (players . orphans . alive) game
&& isNothing (game ^. roleModel)
stageAvailable game ProtectorsTurn = has (players . protectors . alive) game
stageAvailable game ScapegoatsTurn = game ^. scapegoatBlamed
stageAvailable game SeersTurn = has (players . seers . alive) game
stageAvailable _ Sunrise = True
stageAvailable _ Sunset = True
stageAvailable game VillageDrunksTurn =
has (players . villageDrunks . alive) game
&& is thirdRound game
stageAvailable game VillagesTurn = any (is alive) (getAllowedVoters game)
stageAvailable game WerewolvesTurn = has (players . werewolves . alive) game
stageAvailable game WitchsTurn =
has (players . witches . alive) game
&& (not (game ^. healUsed) || not (game ^. poisonUsed))
-- | Creates a new 'Game' with the given players. No validations are performed here, those are left
-- to the binary.
newGame :: Variant -> [Player] -> Game
newGame variant players = Game
{ _variant = variant
, _stage = head stageCycle
, _round = 0
, _players = players
, _boots = Map.empty
, _passed = False
, _allowedVoters = players ^.. names
, _divine = Nothing
, _fallenAngelLynched = False
, _healUsed = False
, _hunterRetaliated = False
, _jesterRevealed = False
, _poison = Nothing
, _poisonUsed = False
, _priorProtect = Nothing
, _protect = Nothing
, _roleModel = Nothing
, _scapegoatBlamed = False
, _see = Nothing
, _votes = Map.empty
}
-- | The traversal of the 'votes' victim's name. This is the player, if they exist, that received
-- the majority of the votes. This could be an empty list depending on whether the votes were in
-- conflict.
votee :: Fold Game Player
votee = folding getVotee
-- | Gets the 'votes' victim's name. This is the player, if they exist, that received the majority
-- of the votes. This could be an empty list depending on whether the votes were in conflict.
getVotee :: Game -> [Player]
getVotee game
| Map.null (game ^. votes) = []
| length result /= 1 = []
| otherwise = game ^.. players . traverse . named (head result)
where
votees = Map.elems $ game ^. votes
result = last $ groupSortOn (length . (`elemIndices` votees)) (nub votees)
-- | The traversal of 'Game's on the first round.
firstRound :: Prism' Game Game
firstRound = prism (set round 0) $ \game -> (if game ^. round == 0 then Right else Left) game
-- | The traversal of 'Game's on the second round.
secondRound :: Prism' Game Game
secondRound = prism (set round 1) $ \game -> (if game ^. round == 1 then Right else Left) game
-- | The traversal of 'Game's on the third round.
thirdRound :: Prism' Game Game
thirdRound = prism (set round 2) $ \game -> (if game ^. round == 2 then Right else Left) game
-- | Gets all the 'allowedVoters' in a game (which is names only) and maps them to their player.
getAllowedVoters :: Game -> [Player]
getAllowedVoters game =
map (\name -> game ^?! players . traverse . named name) (game ^. allowedVoters)
-- | Gets all 'Alive' players that have yet to vote.
getPendingVoters :: Game -> [Player]
getPendingVoters game =
game ^.. players . traverse . alive . filtered ((`Map.notMember` votes') . view name)
where
votes' = game ^. votes
-- | Queries whether anyone has won.
hasAnyoneWon :: Game -> Bool
hasAnyoneWon game = any ($ game) [hasFallenAngelWon, hasVillagersWon, hasWerewolvesWon]
-- | Queries whether the Fallen Angel has won. The Fallen Angel wins if they manage to get
-- themselves lynched by the Villagers.
hasFallenAngelWon :: Game -> Bool
hasFallenAngelWon game = game ^. fallenAngelLynched
-- | Queries whether the 'Villagers' have won. The 'Villagers' win if they are the only players
-- surviving.
--
-- N.B., the Fallen Angel is not considered when determining whether the 'Villagers' have won.
hasVillagersWon :: Game -> Bool
hasVillagersWon = allOf (players . traverse . alive) (\player -> is villager player || is fallenAngel player)
-- | Queries whether the 'Werewolves' have won. The 'Werewolves' win if they are the only players
-- surviving.
hasWerewolvesWon :: Game -> Bool
hasWerewolvesWon = allOf (players . traverse . alive) (is werewolf)