werewolf 1.3.0.1 → 1.3.1.0
raw patch · 36 files changed
+274/−111 lines, 36 files
Files
- CHANGELOG.md +15/−0
- README.md +11/−1
- app/Game/Werewolf/Command/Status.hs +3/−0
- app/Game/Werewolf/Engine.hs +15/−4
- app/Game/Werewolf/Message/Command.hs +10/−1
- app/Game/Werewolf/Message/Engine.hs +27/−13
- app/Game/Werewolf/Util.hs +16/−4
- app/Game/Werewolf/Variant/NoRoleReveal/Engine.hs +11/−2
- app/Game/Werewolf/Variant/Standard/Command.hs +12/−2
- app/Game/Werewolf/Variant/Standard/Engine.hs +19/−15
- app/Werewolf/Command/Start.hs +6/−5
- app/Werewolf/Options.hs +5/−4
- src/Game/Werewolf/Game.hs +18/−6
- src/Game/Werewolf/Player.hs +30/−16
- src/Game/Werewolf/Role.hs +45/−26
- variant/no-role-reveal/engine/general/spiteful-villager-killed.txt +1/−0
- variant/no-role-reveal/engine/lynching/saint-lynched.txt +1/−0
- variant/standard/command/status/alive-marks.txt +1/−0
- variant/standard/command/status/dead-marks.txt +1/−0
- variant/standard/engine/game-over/everyone-lost.txt +1/−0
- variant/standard/engine/general/spiteful-villager-killed.txt +1/−0
- variant/standard/engine/lynching/saint-lynched.txt +1/−0
- variant/standard/engine/new-game/game-variant.txt +1/−0
- variant/standard/engine/new-game/spiteful-ghost-private.txt +0/−1
- variant/standard/engine/new-game/spiteful-ghost-public.txt +0/−1
- variant/standard/role/dullahan/rules.txt +1/−1
- variant/standard/role/saint/description.txt +1/−0
- variant/standard/role/saint/name.txt +1/−0
- variant/standard/role/saint/rules.txt +1/−0
- variant/standard/role/spiteful-ghost/description.txt +0/−1
- variant/standard/role/spiteful-ghost/name.txt +0/−1
- variant/standard/role/spiteful-ghost/rules.txt +0/−1
- variant/standard/role/spiteful-villager/description.txt +2/−0
- variant/standard/role/spiteful-villager/name.txt +1/−0
- variant/standard/role/spiteful-villager/rules.txt +1/−0
- werewolf.cabal +15/−6
CHANGELOG.md view
@@ -7,6 +7,21 @@ ### Upcoming +### v1.3.1.0++*Minor*++* Added a game variant message at the start of a game. ([#235](https://github.com/hjwylde/werewolf/issues/235))+* Added the Saint role. ([#178](https://github.com/hjwylde/werewolf/issues/178))+* Added marks to the Dullahan's status command. ([#236](https://github.com/hjwylde/werewolf/issues/236))+* Changed Spiteful Ghost to be aligned with the Villagers and to start off alive. ([#238](https://github.com/hjwylde/werewolf/issues/238))+* Renamed Spiteful Ghost to Spiteful Villager. ([#241](https://github.com/hjwylde/werewolf/issues/241))+* Added the spiteful-villagers variant. ([#240](https://github.com/hjwylde/werewolf/issues/240))++*Revisions*++* Fix a bug where due to Scapegoat picking 1 player the day never comes again. ([#239](https://github.com/hjwylde/werewolf/issues/239))+ ### v1.3.0.1 *Revisions*
README.md view
@@ -50,7 +50,6 @@ * Dullahan * Fallen Angel-* Spiteful Ghost **The Villagers:** @@ -70,9 +69,11 @@ * Medusa * Oracle * Protector+* Saint * Scapegoat * Seer * Simple Villager+* Spiteful Villager * True Villager * Witch @@ -84,6 +85,15 @@ * Alpha Wolf * Simple Werewolf++#### Variants++Variants alter how the game plays out.++* Standard+* No role knowledge (players are not informed of the roles in play at the start of a game)+* No role reveal (player roles are not revealed upon death)+* Spiteful Villagers (the default Villager role becomes the Spiteful Villager) ### Installing
app/Game/Werewolf/Command/Status.hs view
@@ -18,6 +18,7 @@ ) where import Control.Lens+import Control.Monad.Extra import Control.Monad.State import Control.Monad.Writer @@ -91,6 +92,8 @@ , rolesInGameMessage (Just callerName) game , playersInGameMessage callerName game ]++ whenM (isPlayerDullahan callerName) $ tell [marksInGameMessage callerName game] where currentStageMessage game | has (stage . _GameOver) game = gameIsOverMessage callerName
app/Game/Werewolf/Engine.hs view
@@ -29,7 +29,8 @@ import Data.Maybe -- TODO (hjw): remove Message.Command-import Game.Werewolf.Game hiding (getAllowedVoters, getPendingVoters, hasAnyoneWon)+import Game.Werewolf.Game hiding (getAllowedVoters, getPendingVoters, hasAnyoneWon,+ hasEveryoneLost) import Game.Werewolf.Message.Command import Game.Werewolf.Message.Engine import Game.Werewolf.Player@@ -76,7 +77,7 @@ HuntersTurn2 -> whenM (use hunterRetaliated) advanceStage Lynching -> do- lynchVotee =<< preuse votee+ unlessM (Map.null <$> use votes) $ lynchVotee =<< preuse votee allVoters <- ifM (use jesterRevealed) (uses players $ filter (isn't jester))@@ -196,6 +197,15 @@ fallenAngelLynched .= True tell . (:[]) . playerLynchedMessage votee =<< get+ | is saint votee = do+ killPlayer (votee ^. name)+ tell . (:[]) . playerLynchedMessage votee =<< get++ voterNames <- uses votes (filter (/= votee ^. name) . Map.keys . Map.filter (== votee ^. name))+ forM_ voterNames killPlayer++ voters <- mapM (findPlayerBy_ name) voterNames+ tell . (:[]) . saintLynchedMessage voters =<< get | is werewolf votee = do killPlayer (votee ^. name) tell . (:[]) . werewolfLynchedMessage votee =<< get@@ -223,7 +233,7 @@ advanceStage :: (MonadState Game m, MonadWriter [Message] m) => m () advanceStage = do game <- get- nextStage <- ifM hasAnyoneWon+ nextStage <- ifM (hasAnyoneWon ||^ hasEveryoneLost) (return GameOver) (return . head $ filter (stageAvailable game) (drop1 $ dropWhile (game ^. stage /=) stageCycle)) @@ -234,4 +244,5 @@ tell . stageMessages =<< get checkGameOver :: (MonadState Game m, MonadWriter [Message] m) => m ()-checkGameOver = whenM hasAnyoneWon $ stage .= GameOver >> get >>= tell . gameOverMessages+checkGameOver = whenM (hasAnyoneWon ||^ hasEveryoneLost) $+ stage .= GameOver >> get >>= tell . gameOverMessages
app/Game/Werewolf/Message/Command.hs view
@@ -37,7 +37,8 @@ pingWerewolvesMessage, -- * Status- currentDiurnalTurnMessage, currentNocturnalTurnMessage, gameIsOverMessage, playersInGameMessage,+ currentDiurnalTurnMessage, currentNocturnalTurnMessage, gameIsOverMessage, marksInGameMessage,+ playersInGameMessage, -- * Unvote playerRescindedVoteMessage,@@ -186,6 +187,14 @@ gameIsOverMessage :: Text -> Message 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 playersInGameMessage :: Text -> Game -> Message playersInGameMessage to game = privateMessage to $ T.append alivePlayersText' deadPlayersText'
app/Game/Werewolf/Message/Engine.hs view
@@ -15,10 +15,10 @@ playerRolesMessage, playerBootedMessage, villageDrunkJoinedPackMessages, villageDrunkJoinedVillageMessage, playerTurnedToStoneMessage, playerSeenMessage, playerDivinedMessage, playerDevouredMessage, noPlayerDevouredMessage, scapegoatChoseAllowedVotersMessage, playerPoisonedMessage,- scapegoatLynchedMessage, playerLynchedMessage, noPlayerLynchedMessage, jesterLynchedMessage,+ scapegoatLynchedMessage, saintLynchedMessage, playerLynchedMessage, noPlayerLynchedMessage, jesterLynchedMessage, ferinaGruntsMessage, orphanJoinedPackMessages, playerKilledMessage, playerLostMessage, playerContributedMessage, playerWonMessage, witchsTurnMessages, firstWerewolvesTurnMessages,- villagesTurnMessage, nightFallsMessage, sunriseMessage, villageDrunksTurnMessages,+ villagesTurnMessage, nightFallsMessage, sunriseMessage, villageDrunksTurnMessages, spitefulVillagerKilledMessage, seersTurnMessages, scapegoatsTurnMessage, protectorsTurnMessages, orphansTurnMessages, oraclesTurnMessages, huntersTurnMessages, stageMessages, trueVillagerMessage, beholderMessage, newPlayerMessage, rolesInGameMessage,@@ -58,6 +58,11 @@ , [playerWonMessage fallenAngelsName] , map playerLostMessage (game ^.. players . names \\ [fallenAngelsName]) ]+ | hasEveryoneLost game = concat+ [ [everyoneLostMessage]+ , [playerRolesMessage game]+ , map playerLostMessage (game ^.. players . names)+ ] | otherwise = concat [ [allegianceWonMessage winningAllegiance] , [playerRolesMessage game]@@ -84,6 +89,9 @@ dullahanWonMessage :: Game -> Message dullahanWonMessage = publicMessage . dullahanWonText +everyoneLostMessage :: Message+everyoneLostMessage = publicMessage everyoneLostText+ fallenAngelWonMessage :: Message fallenAngelWonMessage = publicMessage fallenAngelWonText @@ -96,16 +104,20 @@ newGameMessages :: Game -> [Message] newGameMessages game = concat [ [newPlayersInGameMessage game]+ , gameVariantMessages , [rolesInGameMessage Nothing game] , map newPlayerMessage players' , beholderMessages , dullahanMessages- , spitefulGhostMessages' , trueVillagerMessages , stageMessages game ] where players' = game ^. players+ gameVariantMessages =+ [ gameVariantMessage game+ | hasn't (variant . _Standard) game+ ] beholderMessages = [ beholderMessage beholderName game | has beholders players'@@ -117,11 +129,6 @@ | has dullahans players' , dullahanName <- players' ^.. dullahans . name ]- spitefulGhostMessages'- | has spitefulGhosts players' = spitefulGhostMessages spitefulGhostName game- | otherwise = []- where- spitefulGhostName = players' ^?! spitefulGhosts . name trueVillagerMessages = [ trueVillagerMessage game | has trueVillagers players'@@ -130,6 +137,9 @@ newPlayersInGameMessage :: Game -> Message newPlayersInGameMessage = publicMessage . playersInGameText +gameVariantMessage :: Game -> Message+gameVariantMessage = publicMessage . gameVariantText+ rolesInGameMessage :: Maybe Text -> Game -> Message rolesInGameMessage mTo game | has (variant . _NoRoleKnowledge) game = Message mTo $ NoRoleKnowledge.rolesInGameText game@@ -144,11 +154,10 @@ dullahanMessage :: Text -> Game -> Message dullahanMessage to = privateMessage to . dullahanText -spitefulGhostMessages :: Text -> Game -> [Message]-spitefulGhostMessages to game =- [ privateMessage to $ spitefulGhostPrivateText game- , publicMessage $ spitefulGhostPublicText game- ]+spitefulVillagerKilledMessage :: Text -> Game -> Message+spitefulVillagerKilledMessage to game+ | has (variant . _NoRoleReveal) game = privateMessage to $ NoRoleReveal.spitefulVillagerKilledText game+ | otherwise = privateMessage to $ Standard.spitefulVillagerKilledText game trueVillagerMessage :: Game -> Message trueVillagerMessage = publicMessage . trueVillagerText@@ -329,6 +338,11 @@ playerLynchedMessage player game | has (variant . _NoRoleReveal) game = publicMessage $ NoRoleReveal.playerLynchedText player | otherwise = publicMessage $ Standard.playerLynchedText player++saintLynchedMessage :: [Player] -> Game -> Message+saintLynchedMessage voters game+ | has (variant . _NoRoleReveal) game = publicMessage $ NoRoleReveal.saintLynchedText voters+ | otherwise = publicMessage $ Standard.saintLynchedText voters scapegoatLynchedMessage :: Game -> Message scapegoatLynchedMessage = publicMessage . scapegoatLynchedText
app/Game/Werewolf/Util.hs view
@@ -26,14 +26,14 @@ -- ** Queries isGameOver, isHuntersTurn, isOraclesTurn, isOrphansTurn, isProtectorsTurn, isScapegoatsTurn, isSeersTurn, isSunrise, isVillagesTurn, isWerewolvesTurn, isWitchsTurn,- hasAnyoneWon, hasVillagersWon, hasWerewolvesWon,+ hasAnyoneWon, hasVillagersWon, hasWerewolvesWon, hasEveryoneLost, -- * Player -- ** Queries doesPlayerExist,- isPlayerHunter, isPlayerJester, isPlayerOracle, isPlayerOrphan, isPlayerProtector,- isPlayerScapegoat, isPlayerSeer, isPlayerWitch,+ isPlayerDullahan, isPlayerHunter, isPlayerJester, isPlayerOracle, isPlayerOrphan,+ isPlayerProtector, isPlayerScapegoat, isPlayerSeer, isPlayerWitch, isPlayerWerewolf, isPlayerAlive, isPlayerDead, ) where@@ -50,7 +50,8 @@ import Data.Text (Text) import Game.Werewolf.Game hiding (getAllowedVoters, getPendingVoters,- hasAnyoneWon, hasVillagersWon, hasWerewolvesWon)+ hasAnyoneWon, hasEveryoneLost, hasVillagersWon,+ hasWerewolvesWon) import qualified Game.Werewolf.Game as Game import Game.Werewolf.Message.Engine import Game.Werewolf.Player@@ -65,6 +66,8 @@ players . traverse . named name . state .= Dead + whenM (isPlayerSpitefulVillager name) $ tell . (:[]) . spitefulVillagerKilledMessage name =<< get+ removePlayer :: (MonadState Game m, MonadWriter [Message] m) => Text -> m () removePlayer name' = do killPlayer name'@@ -167,9 +170,15 @@ hasWerewolvesWon :: MonadState Game m => m Bool hasWerewolvesWon = gets Game.hasWerewolvesWon +hasEveryoneLost :: MonadState Game m => m Bool+hasEveryoneLost = gets Game.hasEveryoneLost+ doesPlayerExist :: MonadState Game m => Text -> m Bool doesPlayerExist name = hasuse $ players . traverse . named name +isPlayerDullahan :: MonadState Game m => Text -> m Bool+isPlayerDullahan name' = is dullahan <$> findPlayerBy_ name name'+ isPlayerHunter :: MonadState Game m => Text -> m Bool isPlayerHunter name' = is hunter <$> findPlayerBy_ name name' @@ -190,6 +199,9 @@ isPlayerSeer :: MonadState Game m => Text -> m Bool isPlayerSeer name' = is seer <$> findPlayerBy_ name name'++isPlayerSpitefulVillager :: MonadState Game m => Text -> m Bool+isPlayerSpitefulVillager name' = is spitefulVillager <$> findPlayerBy_ name name' isPlayerWitch :: MonadState Game m => Text -> m Bool isPlayerWitch name' = is witch <$> findPlayerBy_ name name'
app/Game/Werewolf/Variant/NoRoleReveal/Engine.hs view
@@ -14,26 +14,35 @@ module Game.Werewolf.Variant.NoRoleReveal.Engine ( -- * General- playerBootedText,+ playerBootedText, spitefulVillagerKilledText, -- * Lynching- playerLynchedText, werewolfLynchedText,+ playerLynchedText, saintLynchedText, werewolfLynchedText, -- * Sunrise playerDevouredText, playerPoisonedText, playerTurnedToStoneText, ) where +import Control.Lens+ import Data.String.Humanise import Data.String.Interpolate.Extra import Data.Text (Text) import Game.Werewolf+import Game.Werewolf.Message playerBootedText :: Player -> Text playerBootedText player = [iFile|variant/no-role-reveal/engine/general/player-booted.txt|] +spitefulVillagerKilledText :: Game -> Text+spitefulVillagerKilledText game = [iFile|variant/no-role-reveal/engine/general/spiteful-villager-killed.txt|]+ playerLynchedText :: Player -> Text playerLynchedText player = [iFile|variant/no-role-reveal/engine/lynching/player-lynched.txt|]++saintLynchedText :: [Player] -> Text+saintLynchedText voters = [iFile|variant/no-role-reveal/engine/lynching/saint-lynched.txt|] werewolfLynchedText :: Player -> Text werewolfLynchedText werewolf = [iFile|variant/no-role-reveal/engine/lynching/werewolf-lynched.txt|]
app/Game/Werewolf/Variant/Standard/Command.hs view
@@ -43,8 +43,8 @@ callerQuitText, -- * Status- alivePlayersText, currentDiurnalTurnText, currentNocturnalTurnText, deadPlayersText,- gameOverText,+ aliveMarksText, alivePlayersText, currentDiurnalTurnText, currentNocturnalTurnText,+ deadMarksText, deadPlayersText, gameOverText, -- * Unvote callerRescindedVoteText,@@ -186,6 +186,11 @@ 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|]+ where+ aliveMarks = game ^.. players . traverse . alive . filtered (\player -> player ^. name `elem` game ^. marks)+ alivePlayersText :: Game -> Text alivePlayersText game = [iFile|variant/standard/command/status/alive-players.txt|] @@ -194,6 +199,11 @@ 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,22 +18,22 @@ druidsTurnText, -- * General- playerBootedText, playerKilledText,+ playerBootedText, playerKilledText, spitefulVillagerKilledText, -- * Game over- allegianceWonText, dullahanWonText, fallenAngelWonText, playerContributedText, playerLostText,- playerRolesText, playerWonText,+ allegianceWonText, dullahanWonText, everyoneLostText, fallenAngelWonText, playerContributedText,+ playerLostText, playerRolesText, playerWonText, -- * Hunter's turn huntersTurnPrivateText, huntersTurnPublicText, -- * Lynching- jesterLynchedText, noPlayerLynchedText, playerLynchedText, scapegoatLynchedText,- werewolfLynchedText,+ jesterLynchedText, noPlayerLynchedText, playerLynchedText, saintLynchedText,+ scapegoatLynchedText, werewolfLynchedText, -- * New game- beholderText, dullahanText, newPlayerText, playersInGameText, rolesInGameText,- spitefulGhostPrivateText, spitefulGhostPublicText, trueVillagerText,+ beholderText, dullahanText, gameVariantText, newPlayerText, playersInGameText, rolesInGameText,+ trueVillagerText, -- * Oracle's turn oraclesTurnPrivateText, oraclesTurnPublicText,@@ -93,12 +93,18 @@ playerKilledText :: Text playerKilledText = [iFile|variant/standard/engine/general/player-killed.txt|] +spitefulVillagerKilledText :: Game -> Text+spitefulVillagerKilledText game = [iFile|variant/standard/engine/general/spiteful-villager-killed.txt|]+ allegianceWonText :: Allegiance -> Text allegianceWonText allegiance = [iFile|variant/standard/engine/game-over/allegiance-won.txt|] dullahanWonText :: Game -> Text dullahanWonText game = [iFile|variant/standard/engine/game-over/dullahan-won.txt|] +everyoneLostText :: Text+everyoneLostText = [iFile|variant/standard/engine/game-over/everyone-lost.txt|]+ fallenAngelWonText :: Text fallenAngelWonText = [iFile|variant/standard/engine/game-over/fallen-angel-won.txt|] @@ -133,6 +139,9 @@ playerLynchedText :: Player -> Text playerLynchedText player = [iFile|variant/standard/engine/lynching/player-lynched.txt|] +saintLynchedText :: [Player] -> Text+saintLynchedText voters = [iFile|variant/standard/engine/lynching/saint-lynched.txt|]+ scapegoatLynchedText :: Game -> Text scapegoatLynchedText game = [iFile|variant/standard/engine/lynching/scapegoat-lynched.txt|] where@@ -149,6 +158,9 @@ dullahanText :: Game -> Text dullahanText game = [iFile|variant/standard/engine/new-game/dullahan.txt|] +gameVariantText :: Game -> Text+gameVariantText game = [iFile|variant/standard/engine/new-game/game-variant.txt|]+ newPlayerText :: Player -> Text newPlayerText player = [iFile|variant/standard/engine/new-game/new-player.txt|] @@ -161,14 +173,6 @@ roles = game ^.. players . traverse . role roleCounts = map (head &&& length) (groupSortOn humanise roles) totalBalance = sumOf (traverse . balance) roles--spitefulGhostPrivateText :: Game -> Text-spitefulGhostPrivateText game = [iFile|variant/standard/engine/new-game/spiteful-ghost-private.txt|]--spitefulGhostPublicText :: Game -> Text-spitefulGhostPublicText game = [iFile|variant/standard/engine/new-game/spiteful-ghost-public.txt|]- where- spitefulGhost = game ^?! players . spitefulGhosts trueVillagerText :: Game -> Text trueVillagerText game = [iFile|variant/standard/engine/new-game/true-villager.txt|]
app/Werewolf/Command/Start.hs view
@@ -58,7 +58,8 @@ Random -> randomExtraRoles $ length playerNames Use roleNames -> useExtraRoles callerName roleNames - let roles = padRoles extraRoles' (length playerNames + 1)+ let defaultVillagerRole = if variant == SpitefulVillagers then spitefulVillagerRole else simpleVillagerRole+ let roles = padRoles extraRoles' (length playerNames + 1) defaultVillagerRole players <- createPlayers (callerName:playerNames) <$> shuffleM roles @@ -85,8 +86,8 @@ findByTag :: Text -> Maybe Role findByTag tag' = restrictedRoles ^? traverse . filteredBy tag tag' -padRoles :: [Role] -> Int -> [Role]-padRoles roles n = roles ++ simpleVillagerRoles ++ simpleWerewolfRoles+padRoles :: [Role] -> Int -> Role -> [Role]+padRoles roles n defaultVillagerRole = roles ++ villagerRoles ++ simpleWerewolfRoles where goal = 3 m = max (n - length roles) 0@@ -96,10 +97,10 @@ -- Little magic here to calculate how many Werewolves and Villagers we want. -- This tries to ensure that the balance of the game is between -3 and 2. simpleWerewolvesCount = (goal - m - startingBalance) `div` (simpleWerewolfBalance - 1) + 1- simpleVillagersCount = m - simpleWerewolvesCount+ villagersCount = m - simpleWerewolvesCount -- N.B., if roles is quite unbalanced then one list will be empty.- simpleVillagerRoles = replicate simpleVillagersCount simpleVillagerRole+ villagerRoles = replicate villagersCount defaultVillagerRole simpleWerewolfRoles = replicate simpleWerewolvesCount simpleWerewolfRole createPlayers :: [Text] -> [Role] -> [Player]
app/Werewolf/Options.hs view
@@ -184,10 +184,11 @@ <*> some (T.pack <$> strArgument (metavar "PLAYER...")) where variantOption = option $ readerAsk >>= \opt -> case opt of- "standard" -> return Standard- "no-role-knowledge" -> return NoRoleKnowledge- "no-role-reveal" -> return NoRoleReveal- _ -> readerError $ "unrecognised variant `" ++ opt ++ "'"+ "standard" -> return Standard+ "no-role-knowledge" -> return NoRoleKnowledge+ "no-role-reveal" -> return NoRoleReveal+ "spiteful-villagers" -> return SpitefulVillagers+ _ -> readerError $ "unrecognised variant `" ++ opt ++ "'" extraRolesOption = fmap (Start.Use . filter (/= T.empty) . T.splitOn "," . T.pack) (strOption $ mconcat [ long "extra-roles", short 'e', metavar "ROLE,..."
src/Game/Werewolf/Game.hs view
@@ -47,6 +47,7 @@ -- ** Queries hasAnyoneWon, hasDullahanWon, hasFallenAngelWon, hasVillagersWon, hasWerewolvesWon,+ hasEveryoneLost, ) where import Control.Lens.Extra@@ -96,13 +97,14 @@ , _votes :: Map Text Text -- ^ Villagers and Werewolves } deriving (Eq, Read, Show) -data Variant = Standard | NoRoleKnowledge | NoRoleReveal+data Variant = Standard | NoRoleKnowledge | NoRoleReveal | SpitefulVillagers deriving (Eq, Read, Show) instance Humanise Variant where humanise Standard = "standard" humanise NoRoleKnowledge = "no role knowledge" humanise NoRoleReveal = "no role reveal"+ humanise SpitefulVillagers = "spiteful villagers" -- | Most of these are fairly self-explainable (the turn stages). 'Sunrise' and 'Sunset' are -- provided as meaningful breaks between the day and night as, for example, a 'VillagesTurn' may@@ -202,7 +204,7 @@ stageAvailable game HuntersTurn2 = has (players . hunters . dead) game && not (game ^. hunterRetaliated)-stageAvailable game Lynching = Map.size (game ^. votes) > 0+stageAvailable _ Lynching = True stageAvailable game OraclesTurn = has (players . oracles . alive) game stageAvailable game OrphansTurn = has (players . orphans . alive) game@@ -305,7 +307,9 @@ -- | Queries whether the Dullahan has won. The Dullahan wins if they manage to eliminate all their -- marks. hasDullahanWon :: Game -> Bool-hasDullahanWon game = has (players . dullahans . alive) game && all (is dead) (getMarks game)+hasDullahanWon game =+ has (players . dullahans . alive) game+ && all (is dead) (getMarks game) -- | Queries whether the Fallen Angel has won. The Fallen Angel wins if they manage to get -- themselves lynched by the Villagers.@@ -318,10 +322,18 @@ -- N.B., the Dullahan and Fallen Angel are not considered when determining whether the 'Villagers' -- have won. hasVillagersWon :: Game -> Bool-hasVillagersWon = allOf (players . traverse . alive)- (\player -> any ($ player) [is villager, is dullahan, is fallenAngel])+hasVillagersWon game =+ not (hasEveryoneLost game)+ && allOf (players . traverse . alive)+ (\player -> any ($ player) [is villager, is dullahan, is fallenAngel]) game -- | Queries whether the 'Werewolves' have won. The 'Werewolves' win if they are the only players -- surviving. hasWerewolvesWon :: Game -> Bool-hasWerewolvesWon = allOf (players . traverse . alive) (is werewolf)+hasWerewolvesWon game =+ not (hasEveryoneLost game)+ && allOf (players . traverse . alive) (is werewolf) game++-- | Queries whether everyone has lost.+hasEveryoneLost :: Game -> Bool+hasEveryoneLost = allOf (players . traverse) (is dead)
src/Game/Werewolf/Player.hs view
@@ -25,8 +25,8 @@ -- ** Traversals alphaWolf, beholder, crookedSenator, druid, dullahan, fallenAngel, hunter, jester, lycan,- medusa, oracle, orphan, protector, scapegoat, seer, simpleVillager, simpleWerewolf,- spitefulGhost, trueVillager, villageDrunk, witch,+ medusa, oracle, orphan, protector, saint, scapegoat, seer, simpleVillager, simpleWerewolf,+ spitefulVillager, trueVillager, villageDrunk, witch, loner, villager, werewolf, -- | The following traversals are provided just as a bit of sugar to avoid continually writing@@ -36,8 +36,8 @@ -- | 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, scapegoats, seers, simpleVillagers,- simpleWerewolves, spitefulGhosts, trueVillagers, villageDrunks, witches,+ lycans, medusas, oracles, orphans, protectors, saints, scapegoats, seers, simpleVillagers,+ simpleWerewolves, spitefulVillagers, trueVillagers, villageDrunks, witches, loners, villagers, werewolves, alive, dead, ) where@@ -74,11 +74,9 @@ makePrisms ''State --- | Creates a new 'Alive' player, with one exception: a 'spitefulGhost' always starts 'Dead'.+-- | Creates a new 'Alive' player. newPlayer :: Text -> Role -> Player-newPlayer name role = Player name role state- where- state = if role == spitefulGhostRole then Dead else Alive+newPlayer name role = Player name role Alive -- | The traversal of 'Player's with an 'alphaWolfRole'. --@@ -184,6 +182,14 @@ protector :: Traversal' Player () protector = role . only protectorRole +-- | The traversal of 'Player's with a 'saintRole'.+--+-- @+-- 'saint' = 'role' . 'only' 'saintRole'+-- @+saint :: Traversal' Player ()+saint = role . only saintRole+ -- | The traversal of 'Player's with a 'scapegoatRole'. -- -- @@@ -216,13 +222,13 @@ simpleWerewolf :: Traversal' Player () simpleWerewolf = role . only simpleWerewolfRole --- | The traversal of 'Player's with a 'spitefulGhostRole'.+-- | The traversal of 'Player's with a 'spitefulVillagerRole'. -- -- @--- 'spitefulGhost' = 'role' . 'only' 'spitefulGhostRole'+-- 'spitefulVillager' = 'role' . 'only' 'spitefulVillagerRole' -- @-spitefulGhost :: Traversal' Player ()-spitefulGhost = role . only spitefulGhostRole+spitefulVillager :: Traversal' Player ()+spitefulVillager = role . only spitefulVillagerRole -- | The traversal of 'Player's with a 'trueVillagerRole'. --@@ -408,6 +414,14 @@ protectors :: Traversable t => Traversal' (t Player) Player protectors = traverse . filtered (is protector) +-- | This 'Traversal' provides the traversal of 'saint' 'Player's.+--+-- @+-- 'saints' = 'traverse' . 'filtered' ('is' 'saint')+-- @+saints :: Traversable t => Traversal' (t Player) Player+saints = traverse . filtered (is saint)+ -- | This 'Traversal' provides the traversal of 'scapegoat' 'Player's. -- -- @@@ -440,13 +454,13 @@ simpleWerewolves :: Traversable t => Traversal' (t Player) Player simpleWerewolves = traverse . filtered (is simpleWerewolf) --- | This 'Traversal' provides the traversal of 'spitefulGhost' 'Player's.+-- | This 'Traversal' provides the traversal of 'spitefulVillager' 'Player's. -- -- @--- 'spitefulGhosts' = 'traverse' . 'filtered' ('is' 'spitefulGhost')+-- 'spitefulVillagers' = 'traverse' . 'filtered' ('is' 'spitefulVillager') -- @-spitefulGhosts :: Traversable t => Traversal' (t Player) Player-spitefulGhosts = traverse . filtered (is spitefulGhost)+spitefulVillagers :: Traversable t => Traversal' (t Player) Player+spitefulVillagers = traverse . filtered (is spitefulVillager) -- | This 'Traversal' provides the traversal of 'trueVillager' 'Player's. --
src/Game/Werewolf/Role.hs view
@@ -42,7 +42,7 @@ -- | The Loners look out for themselves and themselves alone. -- The Loners must complete their own objective.- dullahanRole, fallenAngelRole, spitefulGhostRole,+ dullahanRole, fallenAngelRole, -- *** The Villagers -- | Fraught with fear of the unseen enemy, the Villagers must work together to determine the@@ -51,8 +51,8 @@ -- The Villagers must lynch all of the Werewolves. beholderRole, crookedSenatorRole, druidRole, hunterRole, jesterRole, lycanRole, medusaRole,- oracleRole, protectorRole, scapegoatRole, seerRole, simpleVillagerRole, trueVillagerRole,- witchRole,+ oracleRole, protectorRole, saintRole, scapegoatRole, seerRole, simpleVillagerRole,+ spitefulVillagerRole, trueVillagerRole, witchRole, -- *** The Werewolves -- | Hiding in plain sight, the Werewolves are not a small trifle.@@ -134,11 +134,12 @@ , oracleRole , orphanRole , protectorRole+ , saintRole , scapegoatRole , seerRole , simpleVillagerRole , simpleWerewolfRole- , spitefulGhostRole+ , spitefulVillagerRole , trueVillagerRole , villageDrunkRole , witchRole@@ -147,10 +148,10 @@ -- | A list containing roles that are restricted to a single instance per 'Game'. -- -- @--- 'restrictedRoles' = 'allRoles' \\\\ ['simpleVillagerRole', 'simpleWerewolfRole']+-- 'restrictedRoles' = 'allRoles' \\\\ ['simpleVillagerRole', 'simpleWerewolfRole', 'spitefulVillagerRole'] -- @ restrictedRoles :: [Role]-restrictedRoles = allRoles \\ [simpleVillagerRole, simpleWerewolfRole]+restrictedRoles = allRoles \\ [simpleVillagerRole, simpleWerewolfRole, spitefulVillagerRole] -- | /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/@@ -200,8 +201,8 @@ -- /Dullahan knows your name, for you are then marked for death and you should avoid them at all/ -- /costs. ----- The Dullahan is given a list of player names at the start of the game. They must eliminate all--- of them before the end of the game.+-- The Dullahan is given a list of player names at the start of the game. To win, they must+-- eliminate all of them before the end of the game. dullahanRole :: Role dullahanRole = Role { _tag = "dullahan"@@ -232,24 +233,6 @@ , _rules = T.strip [iFile|variant/standard/role/fallen-angel/rules.txt|] } --- | /In this time of turmoil, it would seem unlikely for the Villagers of Fougères to unanimously/--- /agree on anything. Yet this is not so, for they all agree the village is haunted by a ghost./--- /The vindictive Spiteful Ghost never moved on, rather they remain with the sole purpose of/--- /haunting the village and ensuring that the Villagers never forget what they have done./------ The Spiteful ghost is dead and cannot win, however they know the game's role allocations and--- may haunt the village as they wish.-spitefulGhostRole :: Role-spitefulGhostRole = Role- { _tag = "spiteful-ghost"- , _name = T.strip [iFile|variant/standard/role/spiteful-ghost/name.txt|]- , _allegiance = NoOne- , _balance = 0- , _activity = Diurnal- , _description = T.strip [iFile|variant/standard/role/spiteful-ghost/description.txt|]- , _rules = T.strip [iFile|variant/standard/role/spiteful-ghost/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/@@ -413,6 +396,22 @@ , _rules = T.strip [iFile|variant/standard/role/protector/rules.txt|] } +-- | /The Saint, also historically known as a hallow, is recognized as having an exceptional degree of/+-- /holiness and likeness to God. They are a humble Villager and shine light on these dark times./+-- /Extinguishing this light would not be wise/+--+-- If the Saint is lynched by the village, all who voted for them die.+saintRole :: Role+saintRole = Role+ { _tag = "saint"+ , _name = T.strip [iFile|variant/standard/role/saint/name.txt|]+ , _allegiance = Villagers+ , _balance = 3+ , _activity = Diurnal+ , _description = T.strip [iFile|variant/standard/role/saint/description.txt|]+ , _rules = T.strip [iFile|variant/standard/role/saint/rules.txt|]+ }+ -- | /Werewolves don't just spring up out of the ground! That's where dwarves come from. Clearly/ -- /someone is to blame for this affliction to Fougères. Unluckily for the Scapegoat, since/ -- /no-one actually knows who brought them here, the blame is always laid upon them!/@@ -463,6 +462,26 @@ , _activity = Diurnal , _description = T.strip [iFile|variant/standard/role/simple-villager/description.txt|] , _rules = T.strip [iFile|variant/standard/role/simple-villager/rules.txt|]+ }++-- | /A simple, ordinary townsperson in every way. Some may be cobblers, others bakers or even/+-- /nobles. No matter their differences though, the plight of Werewolves in Fougères unites all/+-- /the Villagers in this unfortunate time./+--+-- /Yet the Spiteful Villager has no loyalty in the afterlife; whoever causes them harm may find/+-- /themselves in trouble./+--+-- When the Spiteful Villager is killed, they are informed of everyone's roles and may haunt the+-- village as they wish.+spitefulVillagerRole :: Role+spitefulVillagerRole = Role+ { _tag = "spiteful-villager"+ , _name = T.strip [iFile|variant/standard/role/spiteful-villager/name.txt|]+ , _allegiance = Villagers+ , _balance = 1+ , _activity = Diurnal+ , _description = T.strip [iFile|variant/standard/role/spiteful-villager/description.txt|]+ , _rules = T.strip [iFile|variant/standard/role/spiteful-villager/rules.txt|] } -- | /The True Villager has a heart and soul as clear as day! Their allegiance and devotion to the/
+ variant/no-role-reveal/engine/general/spiteful-villager-killed.txt view
@@ -0,0 +1,1 @@+Being ethereal seldom has it's benefits. Perhaps however this knowledge of the townsfolks' natures will bring you some joy in the afterlife: #{humanisePlayersWithRoles $ game ^. players}.
+ variant/no-role-reveal/engine/lynching/saint-lynched.txt view
@@ -0,0 +1,1 @@+Furious with the village for murdering them, God enacts revenge upon those responsible: #{humanise voters}.
+ variant/standard/command/status/alive-marks.txt view
@@ -0,0 +1,1 @@+The following marks are still alive: #{humanise aliveMarks}.
+ variant/standard/command/status/dead-marks.txt view
@@ -0,0 +1,1 @@+The following marks are dead: #{humanise deadMarks}.
+ variant/standard/engine/game-over/everyone-lost.txt view
@@ -0,0 +1,1 @@+The game is over! Everyone has died.
+ variant/standard/engine/general/spiteful-villager-killed.txt view
@@ -0,0 +1,1 @@+Being ethereal seldom has it's benefits. Perhaps however this knowledge of the townsfolks' natures will bring you some joy in the afterlife: #{humanisePlayersWithRoles $ game ^.. players . traverse . alive}.
+ variant/standard/engine/lynching/saint-lynched.txt view
@@ -0,0 +1,1 @@+Furious with the village for murdering them, God enacts revenge upon those responsible: #{humanisePlayersWithRoles voters}.
+ variant/standard/engine/new-game/game-variant.txt view
@@ -0,0 +1,1 @@+The game variant is set to "#{humanise $ game ^. variant}".
− variant/standard/engine/new-game/spiteful-ghost-private.txt
@@ -1,1 +0,0 @@-Being ethereal seldom has it's benefits. Perhaps however this knowledge of the townsfolks' natures will bring you some joy in the afterlife: #{humanisePlayersWithRoles $ game ^.. players . traverse . filtered (isn't spitefulGhost)}.
− variant/standard/engine/new-game/spiteful-ghost-public.txt
@@ -1,1 +0,0 @@-In a rash decision to rid the scourge of Werewolves, the Villagers of Fougères lynched an innocent. A foolish move, as now #{humanise spitefulGhost} is just one further horror that plagues Fougères; a ghost that knows each and every one of the Villagers and their darkest secrets.
variant/standard/role/dullahan/rules.txt view
@@ -1,1 +1,1 @@-The Dullahan is given a list of player names at the start of the game. They must eliminate all of them before the end of the game.+The Dullahan is given a list of player names at the start of the game. To win, they must eliminate all of them before the end of the game.
+ variant/standard/role/saint/description.txt view
@@ -0,0 +1,1 @@+The Saint, also historically known as a hallow, is recognized as having an exceptional degree of holiness and likeness to God. They are a humble Villager and shine light on these dark times. Extinguishing this light would not be wise
+ variant/standard/role/saint/name.txt view
@@ -0,0 +1,1 @@+Saint
+ variant/standard/role/saint/rules.txt view
@@ -0,0 +1,1 @@+If the Saint is lynched by the village, all who voted for them die.
− variant/standard/role/spiteful-ghost/description.txt
@@ -1,1 +0,0 @@-In this time of turmoil, it would seem unlikely for the Villagers of Fougères to unanimously agree on anything. Yet this is not so, for they all agree the village is haunted by a ghost. The vindictive Spiteful Ghost never moved on, rather they remain with the sole purpose of haunting the village and ensuring that the Villagers never forget what they have done.
− variant/standard/role/spiteful-ghost/name.txt
@@ -1,1 +0,0 @@-Spiteful Ghost
− variant/standard/role/spiteful-ghost/rules.txt
@@ -1,1 +0,0 @@-The Spiteful ghost is dead and cannot win, however they know the game's role allocations and may haunt the village as they wish.
+ variant/standard/role/spiteful-villager/description.txt view
@@ -0,0 +1,2 @@+A simple, ordinary townsperson in every way. Some may be cobblers, others bakers or even nobles. No matter their differences though, the plight of Werewolves in Fougères unites all the Villagers in this unfortunate time.+Yet the Spiteful Villager has no loyalty in the afterlife; whoever causes them harm may find themselves in trouble.
+ variant/standard/role/spiteful-villager/name.txt view
@@ -0,0 +1,1 @@+Spiteful Villager
+ variant/standard/role/spiteful-villager/rules.txt view
@@ -0,0 +1,1 @@+When the Spiteful Villager is killed, they are informed of everyone's roles and may haunt the village as they wish.
werewolf.cabal view
@@ -1,5 +1,5 @@ name: werewolf-version: 1.3.0.1+version: 1.3.1.0 author: Henry J. Wylde maintainer: public@hjwylde.com@@ -32,7 +32,9 @@ variant/no-role-reveal/command/status/current-nocturnal-turn.txt variant/no-role-reveal/command/status/dead-players.txt variant/no-role-reveal/engine/general/player-booted.txt+ variant/no-role-reveal/engine/general/spiteful-villager-killed.txt variant/no-role-reveal/engine/lynching/player-lynched.txt+ variant/no-role-reveal/engine/lynching/saint-lynched.txt variant/no-role-reveal/engine/lynching/werewolf-lynched.txt variant/no-role-reveal/engine/sunrise/player-devoured.txt variant/no-role-reveal/engine/sunrise/player-poisoned.txt@@ -76,9 +78,11 @@ 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/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/unvote/player-rescinded-vote.txt@@ -88,6 +92,7 @@ variant/standard/engine/druids-turn/start.txt variant/standard/engine/game-over/allegiance-won.txt 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/player-contributed.txt variant/standard/engine/game-over/player-lost.txt@@ -95,20 +100,21 @@ variant/standard/engine/game-over/player-won.txt 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/hunters-turn/start-private.txt variant/standard/engine/hunters-turn/start-public.txt variant/standard/engine/lynching/jester-lynched.txt variant/standard/engine/lynching/no-player-lynched.txt variant/standard/engine/lynching/player-lynched.txt+ variant/standard/engine/lynching/saint-lynched.txt variant/standard/engine/lynching/scapegoat-lynched.txt variant/standard/engine/lynching/werewolf-lynched.txt variant/standard/engine/new-game/beholder.txt variant/standard/engine/new-game/dullahan.txt+ variant/standard/engine/new-game/game-variant.txt variant/standard/engine/new-game/new-player.txt variant/standard/engine/new-game/players-in-game.txt variant/standard/engine/new-game/roles-in-game.txt- variant/standard/engine/new-game/spiteful-ghost-private.txt- variant/standard/engine/new-game/spiteful-ghost-public.txt variant/standard/engine/new-game/true-villager.txt variant/standard/engine/oracles-turn/start-private.txt variant/standard/engine/oracles-turn/start-public.txt@@ -204,6 +210,9 @@ variant/standard/role/protector/description.txt variant/standard/role/protector/name.txt variant/standard/role/protector/rules.txt+ variant/standard/role/saint/description.txt+ variant/standard/role/saint/name.txt+ variant/standard/role/saint/rules.txt variant/standard/role/scapegoat/description.txt variant/standard/role/scapegoat/name.txt variant/standard/role/scapegoat/rules.txt@@ -216,9 +225,9 @@ variant/standard/role/simple-werewolf/description.txt variant/standard/role/simple-werewolf/name.txt variant/standard/role/simple-werewolf/rules.txt- variant/standard/role/spiteful-ghost/description.txt- variant/standard/role/spiteful-ghost/name.txt- variant/standard/role/spiteful-ghost/rules.txt+ variant/standard/role/spiteful-villager/description.txt+ variant/standard/role/spiteful-villager/name.txt+ variant/standard/role/spiteful-villager/rules.txt variant/standard/role/true-villager/description.txt variant/standard/role/true-villager/name.txt variant/standard/role/true-villager/rules.txt