werewolf 1.3.1.0 → 1.4.0.0
raw patch · 49 files changed
+510/−135 lines, 49 files
Files
- CHANGELOG.md +11/−0
- README.md +1/−0
- app/Game/Werewolf/Command/Hunter.hs +1/−0
- app/Game/Werewolf/Command/Necromancer.hs +54/−0
- app/Game/Werewolf/Command/Scapegoat.hs +1/−1
- app/Game/Werewolf/Command/Status.hs +6/−7
- app/Game/Werewolf/Command/Villager.hs +4/−3
- app/Game/Werewolf/Command/Werewolf.hs +1/−0
- app/Game/Werewolf/Command/Witch.hs +1/−0
- app/Game/Werewolf/Engine.hs +14/−13
- app/Game/Werewolf/Message.hs +5/−5
- app/Game/Werewolf/Message/Command.hs +27/−10
- app/Game/Werewolf/Message/Engine.hs +28/−1
- app/Game/Werewolf/Message/Error.hs +6/−3
- app/Game/Werewolf/Util.hs +30/−19
- app/Game/Werewolf/Variant/Standard/Command.hs +30/−16
- app/Game/Werewolf/Variant/Standard/Engine.hs +18/−3
- app/Game/Werewolf/Variant/Standard/Error.hs +5/−1
- app/Main.hs +2/−0
- app/Werewolf/Command/Help.hs +2/−0
- app/Werewolf/Command/Pass.hs +9/−2
- app/Werewolf/Command/Raise.hs +46/−0
- app/Werewolf/Options.hs +5/−0
- src/Data/String/Humanise.hs +1/−1
- src/Game/Werewolf/Game.hs +71/−29
- src/Game/Werewolf/Player.hs +37/−4
- src/Game/Werewolf/Role.hs +50/−9
- variant/no-role-reveal/command/status/dead-players.txt +1/−1
- variant/standard/command/help/necromancer-commands.txt +3/−0
- variant/standard/command/help/necromancers-turn.txt +1/−0
- variant/standard/command/raise/necromancer-raised-dead.txt +1/−0
- variant/standard/command/raise/player-raised-from-dead.txt +1/−0
- variant/standard/command/status/alive-marks.txt +0/−1
- variant/standard/command/status/alive-players.txt +1/−1
- variant/standard/command/status/dead-marks.txt +0/−1
- variant/standard/command/status/marks.txt +1/−0
- variant/standard/engine/game-over/necromancer-won.txt +2/−0
- variant/standard/engine/general/zombies-returned-to-grave.txt +1/−0
- variant/standard/engine/necromancers-turn/start-private.txt +2/−0
- variant/standard/engine/necromancers-turn/start-public.txt +1/−0
- variant/standard/engine/scapegoats-turn/end.txt +1/−1
- variant/standard/error/command/choose/caller-cannot-choose-zombie.txt +1/−0
- variant/standard/role/necromancer/description.txt +1/−0
- variant/standard/role/necromancer/name.txt +1/−0
- variant/standard/role/necromancer/rules.txt +3/−0
- variant/standard/role/zombie/description.txt +1/−0
- variant/standard/role/zombie/name.txt +1/−0
- variant/standard/role/zombie/rules.txt +1/−0
- werewolf.cabal +19/−3
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