packages feed

werewolf 1.3.1.0 → 1.4.0.0

raw patch · 49 files changed

+510/−135 lines, 49 files

Files

CHANGELOG.md view
@@ -7,6 +7,17 @@  ### Upcoming +### v1.4.0.0++*Major*++* Added the Necromancer role. ([#211](https://github.com/hjwylde/werewolf/issues/211))+* Renamed Scapegoat's allowedVoters to chosenVoters. ([#211](https://github.com/hjwylde/werewolf/issues/211))++*Minor*++* Added the Zombie role. ([#211](https://github.com/hjwylde/werewolf/issues/211))+ ### v1.3.1.0  *Minor*
README.md view
@@ -50,6 +50,7 @@  * Dullahan * Fallen Angel+* Necromancer (and Zombie)  **The Villagers:** 
app/Game/Werewolf/Command/Hunter.hs view
@@ -35,6 +35,7 @@     unlessM (isPlayerHunter callerName)     $ throwError [playerCannotDoThatMessage callerName]     unlessM isHuntersTurn                   $ throwError [playerCannotDoThatRightNowMessage callerName]     validatePlayer callerName targetName+    whenM (isPlayerZombie targetName)       $ throwError [playerCannotChooseZombieMessage callerName]      target <- findPlayerBy_ name targetName 
+ app/Game/Werewolf/Command/Necromancer.hs view
@@ -0,0 +1,54 @@+{-|+Module      : Game.Werewolf.Command.Necromancer+Description : Seer commands.++Copyright   : (c) Henry J. Wylde, 2016+License     : BSD3+Maintainer  : public@hjwylde.com++Necromancer commands.+-}++{-# LANGUAGE FlexibleContexts      #-}+{-# LANGUAGE MultiParamTypeClasses #-}++module Game.Werewolf.Command.Necromancer (+    -- * Commands+    passCommand, raiseCommand,+) where++import Control.Lens+import Control.Monad.Except+import Control.Monad.Extra+import Control.Monad.State  hiding (state)+import Control.Monad.Writer++import Data.Text (Text)++import Game.Werewolf+import Game.Werewolf.Command+import Game.Werewolf.Message.Command+import Game.Werewolf.Message.Error+import Game.Werewolf.Util++passCommand :: Text -> Command+passCommand callerName = Command $ do+    validateCommand callerName++    passed .= True++raiseCommand :: Text -> Command+raiseCommand callerName = Command $ do+    validateCommand callerName++    players . traverse . dead . role .= zombieRole+    players . traverse . dead . state .= Alive+    tell . deadRaisedMessages =<< get++    deadRaised .= True++validateCommand :: (MonadError [Message] m, MonadState Game m) => Text -> m ()+validateCommand callerName = do+    validatePlayer callerName callerName+    unlessM (isPlayerNecromancer callerName)    $ throwError [playerCannotDoThatMessage callerName]+    unlessM isNecromancersTurn                  $ throwError [playerCannotDoThatRightNowMessage callerName]
app/Game/Werewolf/Command/Scapegoat.hs view
@@ -38,5 +38,5 @@     whenM (use jesterRevealed &&^ anyM isPlayerJester targetNames) $         throwError [playerCannotChooseJesterMessage callerName] -    allowedVoters   .= targetNames+    chosenVoters    .= targetNames     scapegoatBlamed .= False
app/Game/Werewolf/Command/Status.hs view
@@ -22,11 +22,10 @@ import Control.Monad.State import Control.Monad.Writer -import Data.List import Data.Text (Text)  -- TODO (hjw): remove Message.Engine-import           Game.Werewolf                 hiding (getPendingVoters)+import           Game.Werewolf import           Game.Werewolf.Command import           Game.Werewolf.Message.Command import           Game.Werewolf.Message.Engine@@ -46,6 +45,7 @@     HuntersTurn1        -> pingRole hunterRole     HuntersTurn2        -> pingRole hunterRole     Lynching            -> return ()+    NecromancersTurn    -> pingRole necromancerRole     OraclesTurn         -> pingRole oracleRole     OrphansTurn         -> pingRole orphanRole     ProtectorsTurn      -> pingRole protectorRole@@ -71,18 +71,17 @@  pingVillagers :: (MonadState Game m, MonadWriter [Message] m) => m () pingVillagers = do-    allowedVoterNames <- use allowedVoters-    pendingVoterNames <- toListOf names <$> getPendingVoters+    pendingVoterNames <- toListOf (pendingVoters . name) <$> get      tell [pingVillageMessage]-    tell $ map pingPlayerMessage (allowedVoterNames `intersect` pendingVoterNames)+    tell $ map pingPlayerMessage pendingVoterNames  pingWerewolves :: (MonadState Game m, MonadWriter [Message] m) => m () pingWerewolves = do-    pendingVoters <- getPendingVoters+    pendingVoterNames <- toListOf (pendingVoters . name) <$> get      tell . (:[]) . pingWerewolvesMessage =<< get-    tell $ map pingPlayerMessage (pendingVoters ^.. werewolves . name)+    tell $ map pingPlayerMessage pendingVoterNames  statusCommand :: Text -> Command statusCommand callerName = Command $ do
app/Game/Werewolf/Command/Villager.hs view
@@ -17,7 +17,7 @@     unvoteCommand, voteCommand, ) where -import Control.Lens+import Control.Lens.Extra import Control.Monad.Except import Control.Monad.Extra import Control.Monad.State@@ -50,6 +50,7 @@     validateCommand callerName     whenM (isJust <$> getPlayerVote callerName) $ throwError [playerHasAlreadyVotedMessage callerName]     validatePlayer callerName targetName+    whenM (isPlayerZombie targetName)           $ throwError [playerCannotChooseZombieMessage callerName]      votes %= Map.insert callerName targetName @@ -62,5 +63,5 @@ validateCommand :: (MonadError [Message] m, MonadState Game m) => Text -> m () validateCommand callerName = do     validatePlayer callerName callerName-    whenM (uses allowedVoters (callerName `notElem`))   $ throwError [playerCannotDoThatMessage callerName]-    unlessM isVillagesTurn                              $ throwError [playerCannotDoThatRightNowMessage callerName]+    whenM (hasn'tuse $ allowedVoters . named callerName)    $ throwError [playerCannotDoThatMessage callerName]+    unlessM isVillagesTurn                                  $ throwError [playerCannotDoThatRightNowMessage callerName]
app/Game/Werewolf/Command/Werewolf.hs view
@@ -55,6 +55,7 @@     whenM (isJust <$> getPlayerVote callerName) $ throwError [playerHasAlreadyVotedMessage callerName]     validatePlayer callerName targetName     whenM (isPlayerWerewolf targetName)         $ throwError [playerCannotDevourAnotherWerewolfMessage callerName]+    whenM (isPlayerZombie targetName)           $ throwError [playerCannotChooseZombieMessage callerName]      votes %= Map.insert callerName targetName 
app/Game/Werewolf/Command/Witch.hs view
@@ -50,6 +50,7 @@     validateCommand callerName     whenM (use poisonUsed)                      $ throwError [playerHasAlreadyPoisonedMessage callerName]     validatePlayer callerName targetName+    whenM (isPlayerZombie targetName)           $ throwError [playerCannotChooseZombieMessage callerName]     whenM (hasuse $ votee . named targetName)   $ throwError [playerCannotDoThatMessage callerName]      poison      .= Just targetName
app/Game/Werewolf/Engine.hs view
@@ -29,8 +29,7 @@ import           Data.Maybe  -- TODO (hjw): remove Message.Command-import Game.Werewolf.Game            hiding (getAllowedVoters, getPendingVoters, hasAnyoneWon,-                                      hasEveryoneLost)+import Game.Werewolf.Game            hiding (hasAnyoneWon, hasEveryoneLost) import Game.Werewolf.Message.Command import Game.Werewolf.Message.Engine import Game.Werewolf.Player@@ -79,15 +78,17 @@     Lynching -> do         unlessM (Map.null <$> use votes) $ lynchVotee =<< preuse votee -        allVoters       <- ifM (use jesterRevealed)-            (uses players $ filter (isn't jester))-            (use players)-        allowedVoters   .= allVoters ^.. traverse . alive . name--        votes .= Map.empty+        chosenVoters    .= []+        votes           .= Map.empty          advanceStage +    NecromancersTurn -> do+        whenM (hasuse $ players . necromancers . dead) advanceStage++        whenM (use deadRaised)  advanceStage+        whenM (use passed)      advanceStage+     OraclesTurn -> do         whenM (hasuse $ players . oracles . dead) advanceStage @@ -166,7 +167,7 @@          advanceStage -    VillagesTurn -> whenM (null <$> liftM2 intersect getAllowedVoters getPendingVoters) $ do+    VillagesTurn -> whenM (hasn'tuse pendingVoters) $ do         uses votes Map.toList >>= mapM_ (\(voterName, voteeName) -> do             voter <- findPlayerBy_ name voterName             votee <- findPlayerBy_ name voteeName@@ -176,7 +177,7 @@          advanceStage -    WerewolvesTurn -> whenM (none (is werewolf) <$> getPendingVoters) $ do+    WerewolvesTurn -> whenM (hasn'tuse pendingVoters) $ do         whenM (liftM2 (==) (use protect) (preuses votee $ view name)) $ votes .= Map.empty          advanceStage@@ -198,8 +199,8 @@          tell . (:[]) . playerLynchedMessage votee =<< get     | is saint votee        = do-        killPlayer (votee ^. name)         tell . (:[]) . playerLynchedMessage votee =<< get+        killPlayer (votee ^. name)          voterNames <- uses votes (filter (/= votee ^. name) . Map.keys . Map.filter (== votee ^. name))         forM_ voterNames killPlayer@@ -207,11 +208,11 @@         voters <- mapM (findPlayerBy_ name) voterNames         tell . (:[]) . saintLynchedMessage voters =<< get     | is werewolf votee     = do-        killPlayer (votee ^. name)         tell . (:[]) . werewolfLynchedMessage votee =<< get-    | otherwise             = do         killPlayer (votee ^. name)+    | otherwise             = do         tell . (:[]) . playerLynchedMessage votee =<< get+        killPlayer (votee ^. name) lynchVotee _            = preuse (players . scapegoats . alive) >>= \mScapegoat -> case mScapegoat of     Just scapegoat  -> do         scapegoatBlamed .= True
app/Game/Werewolf/Message.hs view
@@ -19,7 +19,7 @@  module Game.Werewolf.Message (     -- * Utility functions-    humanisePlayerWithRole, humanisePlayersWithRoles, humanisePlayerWithRoleIfTrueVillager,+    humanisePlayerWithRole, humanisePlayersWithRoles, humanisePlayerWithRoleIfKnown,     humanisePlayerWithState, article, conjugateToBe, pluralise, ) where @@ -40,10 +40,10 @@ humanisePlayersWithRoles :: [Player] -> Text humanisePlayersWithRoles = humanise . map humanisePlayerWithRole -humanisePlayerWithRoleIfTrueVillager :: Player -> Text-humanisePlayerWithRoleIfTrueVillager player-    | is trueVillager player    = humanisePlayerWithRole player-    | otherwise                 = humanise player+humanisePlayerWithRoleIfKnown :: Player -> Text+humanisePlayerWithRoleIfKnown player+    | any ($ player) [is trueVillager, is zombie]   = humanisePlayerWithRole player+    | otherwise                                     = humanise player  humanisePlayerWithState :: Player -> Text humanisePlayerWithState player
app/Game/Werewolf/Message/Command.hs view
@@ -28,14 +28,18 @@      -- * Help     gameDescriptionMessage, globalCommandsMessage, helpCommandsMessage, hunterCommandsMessage,-    oracleCommandsMessage, orphanCommandsMessage, protectorCommandsMessage, roleMessage,-    rulesMessage, scapegoatCommandsMessage, seerCommandsMessage, stagesMessage,-    standardCommandsMessage, statusCommandsMessage, witchCommandsMessage,+    necromancerCommandsMessage, oracleCommandsMessage, orphanCommandsMessage,+    protectorCommandsMessage, roleMessage, rulesMessage, scapegoatCommandsMessage,+    seerCommandsMessage, stagesMessage, standardCommandsMessage, statusCommandsMessage,+    witchCommandsMessage,      -- * Ping     pingDiurnalRoleMessage, pingNocturnalRoleMessage, pingPlayerMessage, pingVillageMessage,     pingWerewolvesMessage, +    -- * Raise+    deadRaisedMessages,+     -- * Status     currentDiurnalTurnMessage, currentNocturnalTurnMessage, gameIsOverMessage, marksInGameMessage,     playersInGameMessage,@@ -59,7 +63,7 @@ import Game.Werewolf.Game import Game.Werewolf.Player import Game.Werewolf.Response-import Game.Werewolf.Role+import Game.Werewolf.Role                            hiding (name) import Game.Werewolf.Variant.NoRoleKnowledge.Command as NoRoleKnowledge import Game.Werewolf.Variant.NoRoleReveal.Command    as NoRoleReveal import Game.Werewolf.Variant.Standard.Command        as Standard@@ -95,6 +99,9 @@ hunterCommandsMessage :: Text -> Message hunterCommandsMessage to = privateMessage to hunterCommandsText +necromancerCommandsMessage :: Text -> Message+necromancerCommandsMessage to = privateMessage to necromancerCommandsText+ oracleCommandsMessage :: Text -> Message oracleCommandsMessage to = privateMessage to oracleCommandsText @@ -124,6 +131,8 @@     | isNothing mGame || has (players . orphans . named to) (fromJust mGame)     ] ++ [ villageDrunksTurnText     | isNothing mGame || has (players . villageDrunks . named to) (fromJust mGame)+    ] ++ [ necromancersTurnText+    | isNothing mGame || has (players . necromancers . named to) (fromJust mGame)     ] ++ [ seersTurnText     | isNothing mGame || has (players . seers . named to) (fromJust mGame)     ] ++ [ oraclesTurnText@@ -176,6 +185,19 @@     | has (variant . _NoRoleReveal) game    = publicMessage NoRoleReveal.werewolvesPingedText     | otherwise                             = publicMessage Standard.werewolvesPingedText +deadRaisedMessages :: Game -> [Message]+deadRaisedMessages game =+    necromancerRaisedDeadMessage game+    : map (`playerRaisedFromDeadMessage` game) zombieNames+    where+        zombieNames = game ^.. players . zombies . name++necromancerRaisedDeadMessage :: Game -> Message+necromancerRaisedDeadMessage = publicMessage . necromancerRaisedDeadText++playerRaisedFromDeadMessage :: Text -> Game -> Message+playerRaisedFromDeadMessage to = privateMessage to . playerRaisedFromDeadText+ currentDiurnalTurnMessage :: Text -> Game -> Message currentDiurnalTurnMessage to game = privateMessage to $ Standard.currentDiurnalTurnText game @@ -189,12 +211,7 @@ gameIsOverMessage to = privateMessage to gameOverText  marksInGameMessage :: Text -> Game -> Message-marksInGameMessage to game = privateMessage to $ T.append aliveMarksText' deadMarksText'-    where-        aliveMarksText' = aliveMarksText game-        deadMarksText'-            | hasn't (players . traverse . dead) game   = T.empty-            | otherwise                                 = deadMarksText game+marksInGameMessage to game = privateMessage to $ marksText game  playersInGameMessage :: Text -> Game -> Message playersInGameMessage to game = privateMessage to $ T.append alivePlayersText' deadPlayersText'
app/Game/Werewolf/Message/Engine.hs view
@@ -23,7 +23,7 @@     oraclesTurnMessages, huntersTurnMessages, stageMessages,     trueVillagerMessage, beholderMessage, newPlayerMessage, rolesInGameMessage,     newPlayersInGameMessage, newGameMessages, gameOverMessages, fallenAngelWonMessage,-    werewolfLynchedMessage,+    werewolfLynchedMessage, zombiesReturnedToGraveMessage, ) where  import Control.Lens.Extra@@ -58,6 +58,12 @@         , [playerWonMessage fallenAngelsName]         , map playerLostMessage (game ^.. players . names \\ [fallenAngelsName])         ]+    | hasNecromancerWon game    = concat+        [ [necromancerWonMessage]+        , [playerRolesMessage game]+        , map playerWonMessage (necromancersName:zombieNames)+        , map playerLostMessage (game ^.. players . names \\ (necromancersName:zombieNames))+        ]     | hasEveryoneLost game      = concat         [ [everyoneLostMessage]         , [playerRolesMessage game]@@ -85,6 +91,8 @@          dullahansName       = game ^?! players . dullahans . name         fallenAngelsName    = game ^?! players . fallenAngels . name+        necromancersName    = game ^?! players . necromancers . name+        zombieNames         = game ^.. players . zombies . name  dullahanWonMessage :: Game -> Message dullahanWonMessage = publicMessage . dullahanWonText@@ -95,6 +103,9 @@ fallenAngelWonMessage :: Message fallenAngelWonMessage = publicMessage fallenAngelWonText +necromancerWonMessage :: Message+necromancerWonMessage = publicMessage necromancerWonText+ allegianceWonMessage :: Allegiance -> Message allegianceWonMessage = publicMessage . allegianceWonText @@ -169,6 +180,7 @@     HuntersTurn1        -> huntersTurnMessages game     HuntersTurn2        -> huntersTurnMessages game     Lynching            -> []+    NecromancersTurn    -> necromancersTurnMessages necromancersName game     OraclesTurn         -> oraclesTurnMessages oraclesName game     OrphansTurn         -> orphansTurnMessages orphansName game     ProtectorsTurn      -> protectorsTurnMessages protectorsName game@@ -184,6 +196,7 @@     WitchsTurn          -> witchsTurnMessages game     where         players'            = game ^. players+        necromancersName    = players' ^?! necromancers . name         oraclesName         = players' ^?! oracles . name         orphansName         = players' ^?! orphans . name         protectorsName      = players' ^?! protectors . name@@ -198,6 +211,17 @@     where         hunterName = game ^?! players . hunters . name +necromancersTurnMessages :: Text -> Game -> [Message]+necromancersTurnMessages to game+    | has (variant . _NoRoleKnowledge) game =+        [ privateMessage to necromancersTurnPrivateText ]+    | has (variant . _NoRoleReveal) game    =+        [ privateMessage to necromancersTurnPrivateText ]+    | otherwise                             =+        [ publicMessage necromancersTurnPublicText+        , privateMessage to necromancersTurnPrivateText+        ]+ oraclesTurnMessages :: Text -> Game -> [Message] oraclesTurnMessages to game     | has (variant . _NoRoleKnowledge) game =@@ -387,3 +411,6 @@     where         villageDrunk    = game ^?! players . villageDrunks         werewolves      = game ^.. players . traverse . alive . filtered (is werewolf) \\ [villageDrunk]++zombiesReturnedToGraveMessage :: Game -> Message+zombiesReturnedToGraveMessage = publicMessage . zombiesReturnedToGraveText
app/Game/Werewolf/Message/Error.hs view
@@ -17,7 +17,7 @@     playerHasAlreadyVotedToBootMessage,      -- ** Choose-    playerCannotChooseSelfMessage, playerCannotChooseJesterMessage,+    playerCannotChooseJesterMessage, playerCannotChooseSelfMessage, playerCannotChooseZombieMessage,     playerMustChooseAtLeastOneTargetMessage,      -- ** General@@ -54,11 +54,14 @@ playerHasAlreadyVotedToBootMessage :: Text -> Player -> Message playerHasAlreadyVotedToBootMessage to = privateMessage to . callerAlreadyVotedBootText +playerCannotChooseJesterMessage :: Text -> Message+playerCannotChooseJesterMessage to = privateMessage to callerCannotChooseJesterText+ playerCannotChooseSelfMessage :: Text -> Message playerCannotChooseSelfMessage to = privateMessage to callerCannotChooseSelfText -playerCannotChooseJesterMessage :: Text -> Message-playerCannotChooseJesterMessage to = privateMessage to callerCannotChooseJesterText+playerCannotChooseZombieMessage :: Text -> Message+playerCannotChooseZombieMessage to = privateMessage to callerCannotChooseZombieText  playerMustChooseAtLeastOneTargetMessage :: Text -> Message playerMustChooseAtLeastOneTargetMessage to = privateMessage to noTargetText
app/Game/Werewolf/Util.hs view
@@ -21,19 +21,19 @@      -- ** Searches     findPlayerBy_, getAdjacentAlivePlayers, getFirstAdjacentAliveWerewolf, getPlayerVote,-    getAllowedVoters, getPendingVoters,      -- ** Queries-    isGameOver, isHuntersTurn, isOraclesTurn, isOrphansTurn, isProtectorsTurn, isScapegoatsTurn,-    isSeersTurn, isSunrise, isVillagesTurn, isWerewolvesTurn, isWitchsTurn,-    hasAnyoneWon, hasVillagersWon, hasWerewolvesWon, hasEveryoneLost,+    isGameOver, isHuntersTurn, isNecromancersTurn, isOraclesTurn, isOrphansTurn, isProtectorsTurn,+    isScapegoatsTurn, isSeersTurn, isSunrise, isVillagesTurn, isWerewolvesTurn, isWitchsTurn,+    hasAnyoneWon, hasNecromancerWon, hasVillagersWon, hasWerewolvesWon, hasEveryoneLost,      -- * Player      -- ** Queries     doesPlayerExist,-    isPlayerDullahan, isPlayerHunter, isPlayerJester, isPlayerOracle, isPlayerOrphan,-    isPlayerProtector, isPlayerScapegoat, isPlayerSeer, isPlayerWitch,+    isPlayerDullahan, isPlayerHunter, isPlayerJester, isPlayerNecromancer, isPlayerOracle,+    isPlayerOrphan, isPlayerProtector, isPlayerScapegoat, isPlayerSeer, isPlayerWitch,+    isPlayerZombie,     isPlayerWerewolf,     isPlayerAlive, isPlayerDead, ) where@@ -49,9 +49,8 @@ import           Data.Maybe import           Data.Text  (Text) -import           Game.Werewolf.Game           hiding (getAllowedVoters, getPendingVoters,-                                               hasAnyoneWon, hasEveryoneLost, hasVillagersWon,-                                               hasWerewolvesWon)+import           Game.Werewolf.Game           hiding (hasAnyoneWon, hasEveryoneLost,+                                               hasVillagersWon, hasWerewolvesWon) import qualified Game.Werewolf.Game           as Game import           Game.Werewolf.Message.Engine import           Game.Werewolf.Player@@ -61,13 +60,22 @@ import Prelude hiding (round)  killPlayer :: (MonadState Game m, MonadWriter [Message] m) => Text -> m ()-killPlayer name = do-    tell [playerKilledMessage name]+killPlayer name' = do+    tell [playerKilledMessage name'] -    players . traverse . named name . state .= Dead+    players . traverse . named name' . state .= Dead -    whenM (isPlayerSpitefulVillager name) $ tell . (:[]) . spitefulVillagerKilledMessage name =<< get+    whenM (isPlayerSpitefulVillager name') $+        tell . (:[]) . spitefulVillagerKilledMessage name' =<< get +    whenM (isPlayerNecromancer name') $ do+        zombieNames <- toListOf (players . zombies . name) <$> get++        unless (null zombieNames) $ do+            mapM_ killPlayer zombieNames++            tell . (:[]) . zombiesReturnedToGraveMessage =<< get+ removePlayer :: (MonadState Game m, MonadWriter [Message] m) => Text -> m () removePlayer name' = do     killPlayer name'@@ -119,12 +127,6 @@ getPlayerVote :: MonadState Game m => Text -> m (Maybe Text) getPlayerVote playerName = use $ votes . at playerName -getAllowedVoters :: MonadState Game m => m [Player]-getAllowedVoters = gets Game.getAllowedVoters--getPendingVoters :: MonadState Game m => m [Player]-getPendingVoters = gets Game.getPendingVoters- isGameOver :: MonadState Game m => m Bool isGameOver = hasuse $ stage . _GameOver @@ -134,6 +136,9 @@     , hasuse $ stage . _HuntersTurn2     ] +isNecromancersTurn :: MonadState Game m => m Bool+isNecromancersTurn = hasuse $ stage . _NecromancersTurn+ isOraclesTurn :: MonadState Game m => m Bool isOraclesTurn = hasuse $ stage . _OraclesTurn @@ -185,6 +190,9 @@ isPlayerJester :: MonadState Game m => Text -> m Bool isPlayerJester name' = is jester <$> findPlayerBy_ name name' +isPlayerNecromancer :: MonadState Game m => Text -> m Bool+isPlayerNecromancer name' = is necromancer <$> findPlayerBy_ name name'+ isPlayerOracle :: MonadState Game m => Text -> m Bool isPlayerOracle name' = is oracle <$> findPlayerBy_ name name' @@ -205,6 +213,9 @@  isPlayerWitch :: MonadState Game m => Text -> m Bool isPlayerWitch name' = is witch <$> findPlayerBy_ name name'++isPlayerZombie :: MonadState Game m => Text -> m Bool+isPlayerZombie name' = is zombie <$> findPlayerBy_ name name'  isPlayerWerewolf :: MonadState Game m => Text -> m Bool isPlayerWerewolf name' = is werewolf <$> findPlayerBy_ name name'
app/Game/Werewolf/Variant/Standard/Command.hs view
@@ -28,12 +28,12 @@      -- * Help     druidsTurnText, gameDescriptionText, gameRulesText, globalCommandsText, helpCommandsText,-    hunterCommandsText, huntersTurnText, oracleCommandsText, oraclesTurnText, orphanCommandsText,-    orphansTurnText, protectorCommandsText, protectorsTurnText, roleDescriptionText,-    scapegoatCommandsText, scapegoatsTurnText, seerCommandsText, seersTurnText,-    standardCommandsText, standardCycleText, statusCommandsText, sunriseText, sunsetText,-    villageDrunksTurnText, villagesTurnText, werewolvesTurnText, winConditionText,-    witchCommandsText, witchsTurnText,+    hunterCommandsText, huntersTurnText, necromancerCommandsText, necromancersTurnText,+    oracleCommandsText, oraclesTurnText, orphanCommandsText, orphansTurnText, protectorCommandsText,+    protectorsTurnText, roleDescriptionText, scapegoatCommandsText, scapegoatsTurnText,+    seerCommandsText, seersTurnText, standardCommandsText, standardCycleText, statusCommandsText,+    sunriseText, sunsetText, villageDrunksTurnText, villagesTurnText, werewolvesTurnText,+    winConditionText, witchCommandsText, witchsTurnText,      -- * Ping     diurnalRolePingedText, nocturnalRolePingedText, playerPingedText, villagePingedText,@@ -42,9 +42,12 @@     -- * Quit     callerQuitText, +    -- * Raise+    necromancerRaisedDeadText, playerRaisedFromDeadText,+     -- * Status-    aliveMarksText, alivePlayersText, currentDiurnalTurnText, currentNocturnalTurnText,-    deadMarksText, deadPlayersText, gameOverText,+    alivePlayersText, currentDiurnalTurnText, currentNocturnalTurnText, deadPlayersText,+    gameOverText, marksText,      -- * Unvote     callerRescindedVoteText,@@ -102,6 +105,12 @@ huntersTurnText :: Text huntersTurnText = [iFile|variant/standard/command/help/hunters-turn.txt|] +necromancerCommandsText :: Text+necromancerCommandsText = [iFile|variant/standard/command/help/necromancer-commands.txt|]++necromancersTurnText :: Text+necromancersTurnText = [iFile|variant/standard/command/help/necromancers-turn.txt|]+ oracleCommandsText :: Text oracleCommandsText = [iFile|variant/standard/command/help/oracle-commands.txt|] @@ -186,11 +195,21 @@ callerQuitText :: Player -> Text callerQuitText caller = [iFile|variant/standard/command/quit/caller-quit.txt|] -aliveMarksText :: Game -> Text-aliveMarksText game = [iFile|variant/standard/command/status/alive-marks.txt|]+necromancerRaisedDeadText :: Game -> Text+necromancerRaisedDeadText game = [iFile|variant/standard/command/raise/necromancer-raised-dead.txt|]     where-        aliveMarks = game ^.. players . traverse . alive . filtered (\player -> player ^. name `elem` game ^. marks)+        zombies' = game ^.. players . zombies +playerRaisedFromDeadText :: Game -> Text+playerRaisedFromDeadText game = [iFile|variant/standard/command/raise/player-raised-from-dead.txt|]+    where+        necromancer' = game ^?! players . necromancers++marksText :: Game -> Text+marksText game = [iFile|variant/standard/command/status/marks.txt|]+    where+        marks' = game ^.. players . traverse . filtered (\player -> player ^. name `elem` game ^. marks)+ alivePlayersText :: Game -> Text alivePlayersText game = [iFile|variant/standard/command/status/alive-players.txt|] @@ -199,11 +218,6 @@  currentNocturnalTurnText :: Game -> Text currentNocturnalTurnText game = [iFile|variant/standard/command/status/current-nocturnal-turn.txt|]--deadMarksText :: Game -> Text-deadMarksText game = [iFile|variant/standard/command/status/dead-marks.txt|]-    where-        deadMarks = game ^.. players . traverse . dead . filtered (\player -> player ^. name `elem` game ^. marks)  deadPlayersText :: Game -> Text deadPlayersText game = [iFile|variant/standard/command/status/dead-players.txt|]
app/Game/Werewolf/Variant/Standard/Engine.hs view
@@ -18,11 +18,11 @@     druidsTurnText,      -- * General-    playerBootedText, playerKilledText, spitefulVillagerKilledText,+    playerBootedText, playerKilledText, spitefulVillagerKilledText, zombiesReturnedToGraveText,      -- * Game over-    allegianceWonText, dullahanWonText, everyoneLostText, fallenAngelWonText, playerContributedText,-    playerLostText, playerRolesText, playerWonText,+    allegianceWonText, dullahanWonText, everyoneLostText, fallenAngelWonText, necromancerWonText,+    playerContributedText, playerLostText, playerRolesText, playerWonText,      -- * Hunter's turn     huntersTurnPrivateText, huntersTurnPublicText,@@ -31,6 +31,9 @@     jesterLynchedText, noPlayerLynchedText, playerLynchedText, saintLynchedText,     scapegoatLynchedText, werewolfLynchedText, +    -- * Necromancer's turn+    necromancersTurnPrivateText, necromancersTurnPublicText,+     -- * New game     beholderText, dullahanText, gameVariantText, newPlayerText, playersInGameText, rolesInGameText,     trueVillagerText,@@ -96,6 +99,9 @@ spitefulVillagerKilledText :: Game -> Text spitefulVillagerKilledText game = [iFile|variant/standard/engine/general/spiteful-villager-killed.txt|] +zombiesReturnedToGraveText :: Game -> Text+zombiesReturnedToGraveText game = [iFile|variant/standard/engine/general/zombies-returned-to-grave.txt|]+ allegianceWonText :: Allegiance -> Text allegianceWonText allegiance = [iFile|variant/standard/engine/game-over/allegiance-won.txt|] @@ -108,6 +114,9 @@ fallenAngelWonText :: Text fallenAngelWonText = [iFile|variant/standard/engine/game-over/fallen-angel-won.txt|] +necromancerWonText :: Text+necromancerWonText = [iFile|variant/standard/engine/game-over/necromancer-won.txt|]+ playerContributedText :: Text playerContributedText = [iFile|variant/standard/engine/game-over/player-contributed.txt|] @@ -149,6 +158,12 @@  werewolfLynchedText :: Player -> Text werewolfLynchedText werewolf = [iFile|variant/standard/engine/lynching/werewolf-lynched.txt|]++necromancersTurnPrivateText :: Text+necromancersTurnPrivateText = [iFile|variant/standard/engine/necromancers-turn/start-private.txt|]++necromancersTurnPublicText :: Text+necromancersTurnPublicText = [iFile|variant/standard/engine/necromancers-turn/start-public.txt|]  beholderText :: Game -> Text beholderText game = [iFile|variant/standard/engine/new-game/beholder.txt|]
app/Game/Werewolf/Variant/Standard/Error.hs view
@@ -19,7 +19,8 @@     callerAlreadyVotedBootText,      -- ** Choose-    callerCannotChooseJesterText, callerCannotChooseSelfText, noTargetText,+    callerCannotChooseJesterText, callerCannotChooseSelfText, callerCannotChooseZombieText,+    noTargetText,      -- ** General     callerCannotDoThatText, callerCannotDoThatRightNowText, callerDeadText, noGameRunningText,@@ -59,6 +60,9 @@  callerCannotChooseSelfText :: Text callerCannotChooseSelfText = [iFile|variant/standard/error/command/choose/caller-cannot-choose-self.txt|]++callerCannotChooseZombieText :: Text+callerCannotChooseZombieText = [iFile|variant/standard/error/command/choose/caller-cannot-choose-zombie.txt|]  noTargetText :: Text noTargetText = [iFile|variant/standard/error/command/choose/no-target.txt|]
app/Main.hs view
@@ -33,6 +33,7 @@ import qualified Werewolf.Command.Poison    as Poison import qualified Werewolf.Command.Protect   as Protect import qualified Werewolf.Command.Quit      as Quit+import qualified Werewolf.Command.Raise     as Raise import qualified Werewolf.Command.See       as See import qualified Werewolf.Command.Start     as Start import qualified Werewolf.Command.Status    as Status@@ -70,6 +71,7 @@     Poison options                      -> Poison.handle callerName tag options     Protect options                     -> Protect.handle callerName tag options     Quit                                -> Quit.handle callerName tag+    Raise                               -> Raise.handle callerName tag     See options                         -> See.handle callerName tag options     Start options                       -> Start.handle callerName tag options     Status                              -> Status.handle callerName tag
app/Werewolf/Command/Help.hs view
@@ -82,6 +82,8 @@     , standardCommandsMessage callerName     ] ++ [ hunterCommandsMessage callerName     | isNothing mGame || has (players . hunters . named callerName) (fromJust mGame)+    ] ++ [ necromancerCommandsMessage callerName+    | isNothing mGame || has (players . necromancers . named callerName) (fromJust mGame)     ] ++ [ oracleCommandsMessage callerName     | isNothing mGame || has (players . oracles . named callerName) (fromJust mGame)     ] ++ [ orphanCommandsMessage callerName
app/Werewolf/Command/Pass.hs view
@@ -14,6 +14,7 @@     handle, ) where +import Control.Lens import Control.Monad.Except import Control.Monad.Extra import Control.Monad.Random@@ -24,7 +25,8 @@  import Game.Werewolf import Game.Werewolf.Command-import Game.Werewolf.Command.Witch+import Game.Werewolf.Command.Necromancer as Necromancer+import Game.Werewolf.Command.Witch       as Witch import Game.Werewolf.Engine import Game.Werewolf.Message.Error @@ -38,7 +40,12 @@      game <- readGame tag -    let command = passCommand callerName+    command <- case game ^. stage of+            NecromancersTurn    -> return $ Necromancer.passCommand callerName+            WitchsTurn          -> return $ Witch.passCommand callerName+            _                   -> exitWith failure+                { messages = [playerCannotDoThatRightNowMessage callerName]+                }      result <- runExceptT . runWriterT $ execStateT (apply command >> checkStage >> checkGameOver) game     case result of
+ app/Werewolf/Command/Raise.hs view
@@ -0,0 +1,46 @@+{-|+Module      : Werewolf.Command.Raise+Description : Handler for the raise subcommand.++Copyright   : (c) Henry J. Wylde, 2016+License     : BSD3+Maintainer  : public@hjwylde.com++Handler for the raise subcommand.+-}++module Werewolf.Command.Raise (+    -- * Handle+    handle,+) where++import Control.Monad.Except+import Control.Monad.Extra+import Control.Monad.Random+import Control.Monad.State+import Control.Monad.Writer++import Data.Text (Text)++import Game.Werewolf+import Game.Werewolf.Command+import Game.Werewolf.Command.Necromancer+import Game.Werewolf.Engine+import Game.Werewolf.Message.Error++import Werewolf.System++handle :: (MonadIO m, MonadRandom m) => Text -> Text -> m ()+handle callerName tag = do+    unlessM (doesGameExist tag) $ exitWith failure+        { messages = [noGameRunningMessage callerName]+        }++    game <- readGame tag++    let command = raiseCommand callerName++    result <- runExceptT . runWriterT $ execStateT (apply command >> checkStage >> checkGameOver) game+    case result of+        Left errorMessages      -> exitWith failure { messages = errorMessages }+        Right (game', messages) -> writeOrDeleteGame tag game' >> exitWith success { messages = messages }
app/Werewolf/Options.hs view
@@ -61,6 +61,7 @@     | Poison Poison.Options     | Protect Protect.Options     | Quit+    | Raise     | See See.Options     | Start Start.Options     | Status@@ -111,6 +112,7 @@         , command "poison"      $ info (helper <*> poison)      (fullDesc <> progDesc "Poison a player")         , command "protect"     $ info (helper <*> protect)     (fullDesc <> progDesc "Protect a player")         , command "quit"        $ info (helper <*> quit)        (fullDesc <> progDesc "Quit the current game")+        , command "raise"       $ info (helper <*> raise)       (fullDesc <> progDesc "Raise the dead as Zombies")         , command "see"         $ info (helper <*> see)         (fullDesc <> progDesc "See a player's allegiance")         , command "start"       $ info (helper <*> start)       (fullDesc <> progDesc "Start a new game")         , command "status"      $ info (helper <*> status)      (fullDesc <> progDesc "Get the status of the current game")@@ -169,6 +171,9 @@  quit :: Parser Command quit = pure Quit++raise :: Parser Command+raise = pure Raise  see :: Parser Command see = See . See.Options <$> playerArgument
src/Data/String/Humanise.hs view
@@ -26,6 +26,6 @@     humanise = id  instance Humanise a => Humanise [a] where-    humanise []     = ""+    humanise []     = "no-one"     humanise [word] = humanise word     humanise words  = T.unwords [T.intercalate ", " (map humanise $ init words), "and", humanise $ last words]
src/Game/Werewolf/Game.hs view
@@ -18,17 +18,17 @@ module Game.Werewolf.Game (     -- * Game     Game,-    variant, stage, round, players, boots, allowedVoters, divine, fallenAngelLynched, healUsed,-    hunterRetaliated, jesterRevealed, marks, passed, poison, poisonUsed, priorProtect, protect,-    roleModel, scapegoatBlamed, see, votes,+    variant, stage, round, players, boots, chosenVoters, deadRaised, divine, fallenAngelLynched,+    healUsed, hunterRetaliated, jesterRevealed, marks, passed, poison, poisonUsed, priorProtect,+    protect, roleModel, scapegoatBlamed, see, votes,      Variant(..),     _Standard, _NoRoleKnowledge, _NoRoleReveal,      Stage(..),-    _DruidsTurn, _GameOver, _HuntersTurn1, _HuntersTurn2, _Lynching, _OraclesTurn, _OrphansTurn,-    _ProtectorsTurn, _ScapegoatsTurn, _SeersTurn, _Sunrise, _Sunset, _VillageDrunksTurn,-    _VillagesTurn, _WerewolvesTurn, _WitchsTurn,+    _DruidsTurn, _GameOver, _HuntersTurn1, _HuntersTurn2, _Lynching, _NecromancersTurn,+    _OraclesTurn, _OrphansTurn, _ProtectorsTurn, _ScapegoatsTurn, _SeersTurn, _Sunrise, _Sunset,+    _VillageDrunksTurn, _VillagesTurn, _WerewolvesTurn, _WitchsTurn,     activity,      allStages,@@ -37,17 +37,17 @@     newGame,      -- ** Folds-    votee,+    votee, allowedVoters, pendingVoters,      -- ** Prisms     firstRound, secondRound, thirdRound,      -- ** Searches-    getAllowedVoters, getPendingVoters, getMarks,+    getMarks,      -- ** Queries-    hasAnyoneWon, hasDullahanWon, hasFallenAngelWon, hasVillagersWon, hasWerewolvesWon,-    hasEveryoneLost,+    hasAnyoneWon, hasDullahanWon, hasFallenAngelWon, hasNecromancerWon, hasVillagersWon,+    hasWerewolvesWon, hasEveryoneLost, ) where  import Control.Lens.Extra@@ -79,7 +79,8 @@     , _round              :: Int     , _players            :: [Player]     , _boots              :: Map Text [Text]-    , _allowedVoters      :: [Text]           -- ^ Jester, Scapegoat+    , _chosenVoters       :: [Text]           -- ^ Scapegoat+    , _deadRaised         :: Bool             -- ^ Necromancer     , _divine             :: Maybe Text       -- ^ Oracle     , _fallenAngelLynched :: Bool             -- ^ Fallen Angel     , _healUsed           :: Bool             -- ^ Witch@@ -112,9 +113,9 @@ -- --   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+data Stage  = DruidsTurn | GameOver | HuntersTurn1 | HuntersTurn2 | Lynching | NecromancersTurn+            | OraclesTurn | OrphansTurn | ProtectorsTurn | ScapegoatsTurn | SeersTurn | Sunrise+            | Sunset | VillageDrunksTurn | VillagesTurn | WerewolvesTurn | WitchsTurn     deriving (Eq, Read, Show)  instance Humanise Stage where@@ -123,6 +124,7 @@     humanise HuntersTurn1       = "Hunter's turn"     humanise HuntersTurn2       = "Hunter's turn"     humanise Lynching           = "Lynching"+    humanise NecromancersTurn   = "Necromancer's turn"     humanise OraclesTurn        = "Oracle's turn"     humanise OrphansTurn        = "Orphan's turn"     humanise ProtectorsTurn     = "Protector's turn"@@ -153,6 +155,7 @@         getter HuntersTurn1         = Diurnal         getter HuntersTurn2         = Diurnal         getter Lynching             = Diurnal+        getter NecromancersTurn     = Nocturnal         getter OraclesTurn          = Nocturnal         getter OrphansTurn          = Nocturnal         getter ProtectorsTurn       = Nocturnal@@ -171,6 +174,7 @@     [ Sunset     , OrphansTurn     , VillageDrunksTurn+    , NecromancersTurn     , SeersTurn     , OraclesTurn     , ProtectorsTurn@@ -205,6 +209,9 @@     has (players . hunters . dead) game     && not (game ^. hunterRetaliated) stageAvailable _ Lynching               = True+stageAvailable game NecromancersTurn    =+    has (players . necromancers . alive) game+    && not (game ^. deadRaised) stageAvailable game OraclesTurn         = has (players . oracles . alive) game stageAvailable game OrphansTurn         =     has (players . orphans . alive) game@@ -217,8 +224,8 @@ 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 VillagesTurn        = has allowedVoters game+stageAvailable game WerewolvesTurn      = has (allowedVoters . werewolf) game stageAvailable game WitchsTurn          =     has (players . witches . alive) game     && (not (game ^. healUsed) || not (game ^. poisonUsed))@@ -233,7 +240,8 @@     , _players              = players     , _boots                = Map.empty     , _passed               = False-    , _allowedVoters        = players ^.. names+    , _chosenVoters         = []+    , _deadRaised           = False     , _divine               = Nothing     , _fallenAngelLynched   = False     , _healUsed             = False@@ -267,6 +275,43 @@         votees = Map.elems $ game ^. votes         result = last $ groupSortOn (length . (`elemIndices` votees)) (nub votees) +-- | The traversal of the allowed voters during the 'VillagesTurn' or 'WerewolvesTurn'. In a+--   standard game, this is all 'Alive' players. However there are two scenarios for the+--   'VillagesTurn' that may change this:+--+--   1) if the 'scapegoat' has chosen some 'chosenVoters', it is these players.+--   2) if the 'jester' has been revealed, he may not vote.+allowedVoters :: Fold Game Player+allowedVoters = folding getAllowedVoters++-- | Gets the allowed voters during the 'VillagesTurn' or 'WerewolvesTurn'. In a standard game, this+--   is all 'Alive' players. However there are two scenarios for the 'VillagesTurn' that may change+--   this:+--+--   1) if the 'scapegoat' has chosen some 'chosenVoters', it is these players.+--   2) if the 'jester' has been revealed, he may not vote.+getAllowedVoters :: Game -> [Player]+getAllowedVoters game+    | not . null $ game ^. chosenVoters = filter ((`elem` game ^. chosenVoters) . view name) players'+    | game ^. jesterRevealed            = filter (isn't jester) players'+    | otherwise                         = players'+    where+        players'+            | has (stage . _WerewolvesTurn) game    = game ^.. players . werewolves . alive+            | otherwise                             = game ^.. players . traverse . alive++-- | The traversal of all 'Alive' players that have yet to vote. This is synonymous to @voters -+--   Map.keys votes@+pendingVoters :: Fold Game Player+pendingVoters = folding getPendingVoters++-- | Gets all 'Alive' players that have yet to vote. This is synonymous to @voters - Map.keys+--   votes@+getPendingVoters :: Game -> [Player]+getPendingVoters game = game ^.. allowedVoters . filtered ((`Map.notMember` votes') . view name)+    where+        votes' = game ^. votes+ -- | 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@@ -279,18 +324,6 @@ 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- -- | Gets all the 'marks' in a game (which is names only) and maps them to their player. getMarks :: Game -> [Player] getMarks game = map (\name -> game ^?! players . traverse . named name) (game ^. marks)@@ -300,6 +333,7 @@ hasAnyoneWon game = any ($ game)     [ hasDullahanWon     , hasFallenAngelWon+    , hasNecromancerWon     , hasVillagersWon     , hasWerewolvesWon     ]@@ -315,6 +349,14 @@ --   themselves lynched by the Villagers. hasFallenAngelWon :: Game -> Bool hasFallenAngelWon game = game ^. fallenAngelLynched++-- | Queries whether the Necromancer has won. The 'Necromancer' wins if they and their zombies are+--   the only players surviving.+hasNecromancerWon :: Game -> Bool+hasNecromancerWon game =+    not (hasEveryoneLost game)+    && allOf (players . traverse . alive)+        (\player -> any ($ player) [is necromancer, is zombie]) game  -- | Queries whether the 'Villagers' have won. The 'Villagers' win if they are the only players --   surviving.
src/Game/Werewolf/Player.hs view
@@ -25,8 +25,8 @@      -- ** Traversals     alphaWolf, beholder, crookedSenator, druid, dullahan, fallenAngel, hunter, jester, lycan,-    medusa, oracle, orphan, protector, saint, scapegoat, seer, simpleVillager, simpleWerewolf,-    spitefulVillager, trueVillager, villageDrunk, witch,+    medusa, necromancer, oracle, orphan, protector, saint, scapegoat, seer, simpleVillager,+    simpleWerewolf, spitefulVillager, trueVillager, villageDrunk, witch, zombie,     loner, villager, werewolf,      -- | The following traversals are provided just as a bit of sugar to avoid continually writing@@ -36,8 +36,9 @@     -- | N.B., the following traversals are not legal for the same reason 'filtered' isn't!     named,     alphaWolves, beholders, crookedSenators, druids, dullahans, fallenAngels, hunters, jesters,-    lycans, medusas, oracles, orphans, protectors, saints, scapegoats, seers, simpleVillagers,-    simpleWerewolves, spitefulVillagers, trueVillagers, villageDrunks, witches,+    lycans, medusas, necromancers, oracles, orphans, protectors, saints, scapegoats, seers,+    simpleVillagers, simpleWerewolves, spitefulVillagers, trueVillagers, villageDrunks, witches,+    zombies,     loners, villagers, werewolves,     alive, dead, ) where@@ -158,6 +159,14 @@ medusa :: Traversal' Player () medusa = role . only medusaRole +-- | The traversal of 'Player's with a 'necromancerRole'.+--+-- @+-- 'necromancer' = 'role' . 'only' 'necromancerRole'+-- @+necromancer :: Traversal' Player ()+necromancer = role . only necromancerRole+ -- | The traversal of 'Player's with a 'oracleRole'. -- -- @@@ -254,6 +263,14 @@ witch :: Traversal' Player () witch = role . only witchRole +-- | The traversal of 'Player's with a 'zombieRole'.+--+-- @+-- 'zombie' = 'role' . 'only' 'zombieRole'+-- @+zombie :: Traversal' Player ()+zombie = role . only zombieRole+ -- | The traversal of 'Player's aligned with 'NoOne'. -- -- @@@ -390,6 +407,14 @@ medusas :: Traversable t => Traversal' (t Player) Player medusas = traverse . filtered (is medusa) +-- | This 'Traversal' provides the traversal of 'necromancer' 'Player's.+--+-- @+-- 'necromancers' = 'traverse' . 'filtered' ('is' 'necromancer')+-- @+necromancers :: Traversable t => Traversal' (t Player) Player+necromancers = traverse . filtered (is necromancer)+ -- | This 'Traversal' provides the traversal of 'oracle' 'Player's. -- -- @@@ -485,6 +510,14 @@ -- @ witches :: Traversable t => Traversal' (t Player) Player witches = traverse . filtered (is witch)++-- | This 'Traversal' provides the traversal of 'zombie' 'Player's.+--+-- @+-- 'zombies' = 'traverse' . 'filtered' ('is' 'zombie')+-- @+zombies :: Traversable t => Traversal' (t Player) Player+zombies = traverse . filtered (is zombie)  -- | This 'Traversal' provides the traversal of 'loner' 'Player's. --
src/Game/Werewolf/Role.hs view
@@ -24,7 +24,7 @@     tag, name, allegiance, balance, activity, description, rules,      Allegiance(..),-    _NoOne, _Villagers, _Werewolves,+    _NoOne, _Necromancer, _Villagers, _Werewolves,      Activity(..),     _Diurnal, _Nocturnal,@@ -42,7 +42,7 @@     -- | The Loners look out for themselves and themselves alone.      --   The Loners must complete their own objective.-    dullahanRole, fallenAngelRole,+    dullahanRole, fallenAngelRole, necromancerRole, zombieRole,      -- *** The Villagers     -- | Fraught with fear of the unseen enemy, the Villagers must work together to determine the@@ -89,13 +89,14 @@  -- | The 'NoOne' allegiance is used for the Loners. It is not used to determine who has won (i.e., --   if one Loner wins, the others still lose).-data Allegiance = NoOne | Villagers | Werewolves+data Allegiance = NoOne | Necromancer | Villagers | Werewolves     deriving (Eq, Read, Show)  instance Humanise Allegiance where-    humanise NoOne      = "no-one"-    humanise Villagers  = "Villagers"-    humanise Werewolves = "Werewolves"+    humanise NoOne          = "no-one"+    humanise Necromancer    = "Necromancer"+    humanise Villagers      = "Villagers"+    humanise Werewolves     = "Werewolves"  -- | Defines whether a role is diurnal or nocturnal. I.e., if the role's turn occurs during the day --   or night.@@ -131,6 +132,7 @@     , jesterRole     , lycanRole     , medusaRole+    , necromancerRole     , oracleRole     , orphanRole     , protectorRole@@ -143,15 +145,16 @@     , trueVillagerRole     , villageDrunkRole     , witchRole+    , zombieRole     ]  -- | A list containing roles that are restricted to a single instance per 'Game'. -- --   @---   'restrictedRoles' = 'allRoles' \\\\ ['simpleVillagerRole', 'simpleWerewolfRole', 'spitefulVillagerRole']+--   'restrictedRoles' = 'allRoles' \\\\ ['simpleVillagerRole', 'simpleWerewolfRole', 'spitefulVillagerRole', 'zombieRole'] --   @ restrictedRoles :: [Role]-restrictedRoles = allRoles \\ [simpleVillagerRole, simpleWerewolfRole, spitefulVillagerRole]+restrictedRoles = allRoles \\ [simpleVillagerRole, simpleWerewolfRole, spitefulVillagerRole, zombieRole]  -- | /Abandoned by their parents as a child, with no-one wanting to look after another mouth to/ --   /feed, the Orphan was left to fend for themself. No-one looked twice at the Orphan and even/@@ -233,6 +236,44 @@     , _rules        = T.strip [iFile|variant/standard/role/fallen-angel/rules.txt|]     } +-- | /The dead are feared among the living; the dead outnumber the living. In most villages the/+--   /dead remain that way, but not when the Necromancer is present. The Necromancer devoted their/+--   /life to learning black magic and methods of bringing people back to life. Unfortunately this/+--   /art is hard to perfect and all they can manage to bring back are soulless Zombies./+--+--   Once per game the Necromancer can choose to resurrect all dead players as Zombies. The Zombies+--   are aligned with the Necromancer and they cannot be lynched or devoured.+--+--   If the Necromancer is killed, all Zombies die with them.+--+--   The Necromancer and Zombies win if they are the last ones alive.+necromancerRole :: Role+necromancerRole = Role+    { _tag          = "necromancer"+    , _name         = T.strip [iFile|variant/standard/role/necromancer/name.txt|]+    , _allegiance   = NoOne+    , _balance      = 0+    , _activity     = Nocturnal+    , _description  = T.strip [iFile|variant/standard/role/necromancer/description.txt|]+    , _rules        = T.strip [iFile|variant/standard/role/necromancer/rules.txt|]+    }++-- | /A loyal follower of the Necromancer. A Zombie has no mind of its own and blindly obeys every/+--   /command of their master./+--+--   A Zombie wins with the Necromancer. They cannot be killed, however they die when the+--   Necromancer dies.+zombieRole :: Role+zombieRole = Role+    { _tag          = "zombie"+    , _name         = T.strip [iFile|variant/standard/role/zombie/name.txt|]+    , _allegiance   = Necromancer+    , _balance      = 0+    , _activity     = Nocturnal+    , _description  = T.strip [iFile|variant/standard/role/zombie/description.txt|]+    , _rules        = T.strip [iFile|variant/standard/role/zombie/rules.txt|]+    }+ -- | /Awareness comes easy to the Beholder. They listen to their senses and trust their hunches./ --   /Over the years the Beholder has grown to know a certain few of the village just by paying/ --   /attention. Little cues here and there, the way someone talks, the way they move - it all/@@ -425,7 +466,7 @@     { _tag          = "scapegoat"     , _name         = T.strip [iFile|variant/standard/role/scapegoat/name.txt|]     , _allegiance   = Villagers-    , _balance      = 0+    , _balance      = 1     , _activity     = Diurnal     , _description  = T.strip [iFile|variant/standard/role/scapegoat/description.txt|]     , _rules        = T.strip [iFile|variant/standard/role/scapegoat/rules.txt|]
variant/no-role-reveal/command/status/dead-players.txt view
@@ -1,1 +1,1 @@-The following players are dead: #{humanise $ map humanisePlayerWithRoleIfTrueVillager (game ^..  players . traverse . dead)}.+The following players are dead: #{humanise $ map humanisePlayerWithRoleIfKnown (game ^..  players . traverse . dead)}.
+ variant/standard/command/help/necromancer-commands.txt view
@@ -0,0 +1,3 @@+#{humanise necromancerRole} commands:+- `raise`+- `pass`
+ variant/standard/command/help/necromancers-turn.txt view
@@ -0,0 +1,1 @@+- The #{humanise necromancerRole} wakes up and may raise the dead.
+ variant/standard/command/raise/necromancer-raised-dead.txt view
@@ -0,0 +1,1 @@+A dark cloud falls upon Fougères, as the #{humanise necromancerRole} begins to raise #{humanise zombies'} from the dead.
+ variant/standard/command/raise/player-raised-from-dead.txt view
@@ -0,0 +1,1 @@+#{humanise necromancer'} has raised you from the dead, bringing you back as a #{humanise zombieRole}. Abandon your past life and embrace your new master; follow them to victory!
− variant/standard/command/status/alive-marks.txt
@@ -1,1 +0,0 @@-The following marks are still alive: #{humanise aliveMarks}.
variant/standard/command/status/alive-players.txt view
@@ -1,1 +1,1 @@-The following players are still alive: #{humanise $ map humanisePlayerWithRoleIfTrueVillager (game ^.. players . traverse . alive)}.+The following players are still alive: #{humanise $ map humanisePlayerWithRoleIfKnown (game ^.. players . traverse . alive)}.
− variant/standard/command/status/dead-marks.txt
@@ -1,1 +0,0 @@-The following marks are dead: #{humanise deadMarks}.
+ variant/standard/command/status/marks.txt view
@@ -0,0 +1,1 @@+Your marks are as follows: #{humanise $ map humanisePlayerWithState marks'}.
+ variant/standard/engine/game-over/necromancer-won.txt view
@@ -0,0 +1,2 @@+Long live the dead!+The game is over! The #{humanise necromancerRole} has won.
+ variant/standard/engine/general/zombies-returned-to-grave.txt view
@@ -0,0 +1,1 @@+With the #{humanise necromancerRole} gone, there is nothing keeping #{humanise $ game ^.. players . zombies} in this world. They begin to rot and return to whence they came.
+ variant/standard/engine/necromancers-turn/start-private.txt view
@@ -0,0 +1,2 @@+Would you like to `raise` the dead?+Type `pass` to end your turn.
+ variant/standard/engine/necromancers-turn/start-public.txt view
@@ -0,0 +1,1 @@+The #{humanise necromancerRole} wakes up.
variant/standard/engine/scapegoats-turn/end.txt view
@@ -1,1 +1,1 @@-On the next day only #{humanise $ game ^. allowedVoters} shall be allowed to vote. The Town Crier, realising how foolish it was to kill the #{humanise scapegoatRole}, grants them this wish.+On the next day only #{humanise $ game ^. chosenVoters} shall be allowed to vote. The Town Crier, realising how foolish it was to kill the #{humanise scapegoatRole}, grants them this wish.
+ variant/standard/error/command/choose/caller-cannot-choose-zombie.txt view
@@ -0,0 +1,1 @@+You cannot choose a #{humanise zombieRole}!
+ variant/standard/role/necromancer/description.txt view
@@ -0,0 +1,1 @@+The dead are feared among the living; the dead outnumber the living. In most villages the dead remain that way, but not when the Necromancer is present. The Necromancer devoted their life to learning black magic and methods of bringing people back to life. Unfortunately this art is hard to perfect and all they can manage to bring back are soulless Zombies.
+ variant/standard/role/necromancer/name.txt view
@@ -0,0 +1,1 @@+Necromancer
+ variant/standard/role/necromancer/rules.txt view
@@ -0,0 +1,3 @@+Once per game the Necromancer can choose to resurrect all dead players as Zombies. The Zombies are aligned with the Necromancer and they cannot be lynched or devoured.+If the Necromancer is killed, all Zombies die with them.+The Necromancer and Zombies win if they are the last ones alive.
+ variant/standard/role/zombie/description.txt view
@@ -0,0 +1,1 @@+A loyal follower of the Necromancer. A Zombie has no mind of its own and blindly obeys every command of their master.
+ variant/standard/role/zombie/name.txt view
@@ -0,0 +1,1 @@+Zombie
+ variant/standard/role/zombie/rules.txt view
@@ -0,0 +1,1 @@+A Zombie wins with the Necromancer. They cannot be killed, however they die when the Necromancer dies.
werewolf.cabal view
@@ -1,5 +1,5 @@ name:           werewolf-version:        1.3.1.0+version:        1.4.0.0  author:         Henry J. Wylde maintainer:     public@hjwylde.com@@ -49,6 +49,8 @@     variant/standard/command/help/help-commands.txt     variant/standard/command/help/hunter-commands.txt     variant/standard/command/help/hunters-turn.txt+    variant/standard/command/help/necromancer-commands.txt+    variant/standard/command/help/necromancers-turn.txt     variant/standard/command/help/oracle-commands.txt     variant/standard/command/help/oracles-turn.txt     variant/standard/command/help/orphan-commands.txt@@ -78,13 +80,14 @@     variant/standard/command/ping/village-pinged.txt     variant/standard/command/ping/werewolves-pinged.txt     variant/standard/command/quit/caller-quit.txt-    variant/standard/command/status/alive-marks.txt+    variant/standard/command/raise/necromancer-raised-dead.txt+    variant/standard/command/raise/player-raised-from-dead.txt     variant/standard/command/status/alive-players.txt     variant/standard/command/status/current-diurnal-turn.txt     variant/standard/command/status/current-nocturnal-turn.txt-    variant/standard/command/status/dead-marks.txt     variant/standard/command/status/dead-players.txt     variant/standard/command/status/game-over.txt+    variant/standard/command/status/marks.txt     variant/standard/command/unvote/player-rescinded-vote.txt     variant/standard/command/version/engine-version.txt     variant/standard/command/vote/player-made-devour-vote.txt@@ -94,6 +97,7 @@     variant/standard/engine/game-over/dullahan-won.txt     variant/standard/engine/game-over/everyone-lost.txt     variant/standard/engine/game-over/fallen-angel-won.txt+    variant/standard/engine/game-over/necromancer-won.txt     variant/standard/engine/game-over/player-contributed.txt     variant/standard/engine/game-over/player-lost.txt     variant/standard/engine/game-over/player-roles.txt@@ -101,6 +105,7 @@     variant/standard/engine/general/player-booted.txt     variant/standard/engine/general/player-killed.txt     variant/standard/engine/general/spiteful-villager-killed.txt+    variant/standard/engine/general/zombies-returned-to-grave.txt     variant/standard/engine/hunters-turn/start-private.txt     variant/standard/engine/hunters-turn/start-public.txt     variant/standard/engine/lynching/jester-lynched.txt@@ -109,6 +114,8 @@     variant/standard/engine/lynching/saint-lynched.txt     variant/standard/engine/lynching/scapegoat-lynched.txt     variant/standard/engine/lynching/werewolf-lynched.txt+    variant/standard/engine/necromancers-turn/start-private.txt+    variant/standard/engine/necromancers-turn/start-public.txt     variant/standard/engine/new-game/beholder.txt     variant/standard/engine/new-game/dullahan.txt     variant/standard/engine/new-game/game-variant.txt@@ -153,6 +160,7 @@     variant/standard/error/command/boot/caller-already-voted-boot.txt     variant/standard/error/command/choose/caller-cannot-choose-jester.txt     variant/standard/error/command/choose/caller-cannot-choose-self.txt+    variant/standard/error/command/choose/caller-cannot-choose-zombie.txt     variant/standard/error/command/choose/no-target.txt     variant/standard/error/command/general/caller-cannot-do-that-right-now.txt     variant/standard/error/command/general/caller-cannot-do-that.txt@@ -201,6 +209,9 @@     variant/standard/role/medusa/description.txt     variant/standard/role/medusa/name.txt     variant/standard/role/medusa/rules.txt+    variant/standard/role/necromancer/description.txt+    variant/standard/role/necromancer/name.txt+    variant/standard/role/necromancer/rules.txt     variant/standard/role/oracle/description.txt     variant/standard/role/oracle/name.txt     variant/standard/role/oracle/rules.txt@@ -237,6 +248,9 @@     variant/standard/role/witch/description.txt     variant/standard/role/witch/name.txt     variant/standard/role/witch/rules.txt+    variant/standard/role/zombie/description.txt+    variant/standard/role/zombie/name.txt+    variant/standard/role/zombie/rules.txt  source-repository head     type:       git@@ -250,6 +264,7 @@         Game.Werewolf.Command         Game.Werewolf.Command.Global         Game.Werewolf.Command.Hunter+        Game.Werewolf.Command.Necromancer         Game.Werewolf.Command.Oracle         Game.Werewolf.Command.Orphan         Game.Werewolf.Command.Protector@@ -286,6 +301,7 @@         Werewolf.Command.Poison         Werewolf.Command.Protect         Werewolf.Command.Quit+        Werewolf.Command.Raise         Werewolf.Command.See         Werewolf.Command.Start         Werewolf.Command.Status