packages feed

werewolf 0.3.2.0 → 0.3.3.0

raw patch · 14 files changed

+171/−38 lines, 14 files

Files

CHANGELOG.md view
@@ -2,6 +2,17 @@  #### Upcoming +#### v0.3.3.0++*Minor*++* Added a Scapegoat role. ([#40](https://github.com/hjwylde/werewolf/issues/40))+* Added a `status` command. ([#18](https://github.com/hjwylde/werewolf/issues/18))++*Revisions*++* Added `--` to help description of `interpret`. ([#60](https://github.com/hjwylde/werewolf/issues/60))+ #### v0.3.2.0  *Minor*
README.md view
@@ -20,6 +20,7 @@ #### Roles  The current implemented roles are:+* Scapegoat. * Seer. * Villager. * Werewolf.
app/Main.hs view
@@ -27,6 +27,7 @@ import qualified Werewolf.Commands.Quit      as Quit import qualified Werewolf.Commands.See       as See import qualified Werewolf.Commands.Start     as Start+import qualified Werewolf.Commands.Status    as Status import qualified Werewolf.Commands.Vote      as Vote import           Werewolf.Options @@ -52,4 +53,5 @@     Quit                                -> Quit.handle callerName     See options                         -> See.handle callerName options     Start options                       -> Start.handle callerName options+    Status                              -> Status.handle callerName     Vote options                        -> Vote.handle callerName options
app/Werewolf/Commands/Help.hs view
@@ -73,6 +73,9 @@     "start [--extra-roles ROLE,...] PLAYER ...",     "- Starts a new game with the given players and extra roles. A game requires at least 7 players."     ], [+    "status",+    "- Gets the status of the current game."+    ], [     "vote PLAYER",     T.unwords [         "- Vote against a player.",
app/Werewolf/Options.hs view
@@ -48,6 +48,7 @@     | Quit     | See See.Options     | Start Start.Options+    | Status     | Vote Vote.Options     deriving (Eq, Show) @@ -83,6 +84,7 @@         command "quit"      $ info (helper <*> quit)        (fullDesc <> progDesc "Quit the current game"),         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"),         command "vote"      $ info (helper <*> vote)        (fullDesc <> progDesc "Vote against a player")         ]) @@ -99,7 +101,7 @@         ])  interpret :: Parser Command-interpret = Interpret . Interpret.Options <$> many (T.pack <$> strArgument (metavar "COMMAND ARG..."))+interpret = Interpret . Interpret.Options <$> many (T.pack <$> strArgument (metavar "-- COMMAND ARG..."))  quit :: Parser Command quit = pure Quit@@ -115,6 +117,9 @@         help "Specify the extra roles to include"         ])     <*> many (T.pack <$> strArgument (metavar "PLAYER..."))++status :: Parser Command+status = pure Status  vote :: Parser Command vote = Vote . Vote.Options . T.pack <$> strArgument (metavar "PLAYER")
src/Game/Werewolf/Command.hs view
@@ -17,7 +17,7 @@     Command(..),      -- ** Instances-    devourVoteCommand, lynchVoteCommand, noopCommand, quitCommand, seeCommand,+    devourVoteCommand, lynchVoteCommand, noopCommand, quitCommand, seeCommand, statusCommand, ) where  import Control.Lens         hiding (only)@@ -27,9 +27,12 @@ import Control.Monad.Writer  import qualified Data.Map  as Map+import Data.List import           Data.Text (Text)+import           qualified Data.Text as T  import Game.Werewolf.Engine+import Game.Werewolf.Role hiding (Werewolves, Villagers, name, _name, findByName_) import Game.Werewolf.Player hiding (doesPlayerExist) import Game.Werewolf.Game     hiding (isGameOver, isSeersTurn, isVillagersTurn, isWerewolvesTurn,                                killPlayer)@@ -81,6 +84,37 @@     validatePlayer callerName targetName      sees %= Map.insert callerName targetName++statusCommand :: Text -> Command+statusCommand callerName = Command $ use turn >>= \turn' -> case turn' of+    Seers       -> do+        game <- get++        tell $ standardStatusMessages turn' (game ^. players)+    Villagers   -> do+        game <- get++        tell $ standardStatusMessages turn' (game ^. players)+        tell [waitingOnMessage callerName $ filter (flip Map.notMember (game ^. votes) . _name) (filterAlive $ game ^. players)]+    Werewolves  -> do+        unlessM (doesPlayerExist callerName) $ throwError [playerDoesNotExistMessage callerName callerName]++        game <- get++        tell $ standardStatusMessages turn' (game ^. players)+        whenM (isPlayerWerewolf callerName) $ tell [waitingOnMessage callerName $ filter (flip Map.notMember (game ^. votes) . _name) (filterAlive . filterWerewolves $ game ^. players)]+    NoOne       -> do+        aliveAllegiances <- uses players $ nub . map (_allegiance . _role) . filterAlive++        case aliveAllegiances of+            [allegiance]    -> tell [gameOverMessage . Just . T.pack $ show allegiance]+            _               -> tell [gameOverMessage Nothing]+    where+        standardStatusMessages turn players = [+            currentTurnMessage callerName turn,+            rolesInGameMessage (Just [callerName]) $ map _role players,+            playersInGameMessage callerName players+            ]  validatePlayer :: (MonadError [Message] m, MonadState Game m) => Text -> Text -> m () validatePlayer callerName name = do
src/Game/Werewolf/Engine.hs view
@@ -89,8 +89,17 @@              let mLynchedName = only . last $ groupSortOn (length . flip elemIndices (Map.elems votes')) (nub $ Map.elems votes')             case mLynchedName of-                Nothing             -> tell [noPlayerLynchedMessage]-                Just lynchedName    -> do+                Nothing                -> do+                    aliveScapegoats <- uses players (filterAlive . filterScapegoats)++                    let mScapegoat = only aliveScapegoats+                    case mScapegoat of+                        Nothing        -> tell [noPlayerLynchedMessage]+                        Just scapegoat -> do+                            killPlayer scapegoat+                            tell [scapegoatLynchedMessage (scapegoat ^. name)]++                Just lynchedName       -> do                     target <- uses players (findByName_ lynchedName)                      killPlayer target@@ -143,10 +152,10 @@ checkGameOver = do     aliveAllegiances <- uses players $ nub . map (_allegiance . _role) . filterAlive -    case length aliveAllegiances of-        0 -> turn .= NoOne >> tell [gameOverMessage Nothing]-        1 -> turn .= NoOne >> tell [gameOverMessage . Just . T.pack . show $ head aliveAllegiances]-        _ -> return ()+    case aliveAllegiances of+        []              -> turn .= NoOne >> tell [gameOverMessage Nothing]+        [allegiance]    -> turn .= NoOne >> tell [gameOverMessage . Just . T.pack $ show allegiance]+        _               -> return ()  startGame :: (MonadError [Message] m, MonadWriter [Message] m) => Text -> [Player] -> m Game startGame callerName players = do
src/Game/Werewolf/Player.hs view
@@ -20,10 +20,10 @@     findByName, findByName_,      -- ** Filters-    filterSeers, filterVillagers, filterWerewolves,+    filterSeers, filterVillagers, filterWerewolves, filterScapegoats,      -- ** Queries-    doesPlayerExist, isSeer, isVillager, isWerewolf, isAlive, isDead,+    doesPlayerExist, isSeer, isVillager, isWerewolf, isScapegoat, isAlive, isDead,      -- * State     State(..),@@ -69,6 +69,9 @@ filterWerewolves :: [Player] -> [Player] filterWerewolves = filter isWerewolf +filterScapegoats :: [Player] -> [Player]+filterScapegoats = filter isScapegoat+ doesPlayerExist :: Text -> [Player] -> Bool doesPlayerExist name = isJust . findByName name @@ -80,6 +83,9 @@  isWerewolf :: Player -> Bool isWerewolf player = player ^. role == werewolfRole++isScapegoat :: Player -> Bool+isScapegoat player = player ^. role == scapegoatRole  isAlive :: Player -> Bool isAlive player = player ^. state == Alive
src/Game/Werewolf/Response.hs view
@@ -30,11 +30,15 @@     -- ** Generic messages     newGameMessages, turnMessages, nightFallsMessage, gameOverMessage, playerQuitMessage, +    -- ** Status messages+    currentTurnMessage, rolesInGameMessage, playersInGameMessage, waitingOnMessage,+     -- ** Seers turn messages     seersTurnMessages, playerSeenMessage,      -- ** Villagers turn messages     villagersTurnMessage, playerMadeLynchVoteMessage, playerLynchedMessage, noPlayerLynchedMessage,+    scapegoatLynchedMessage,      -- ** Werewolves turn messages     werewolvesTurnMessages, playerMadeDevourVoteMessage, playerDevouredMessage,@@ -123,28 +127,17 @@ privateMessage to = Message (Just to)  newGameMessages :: Game -> [Message]-newGameMessages game = [playersInGameMessage players', rolesInGameMessage $ map _role players'] ++ map (newPlayerMessage players') players' ++ [nightFallsMessage] ++ turnMessages turn' players'+newGameMessages game = [newPlayersInGameMessage players', rolesInGameMessage Nothing $ map _role players'] ++ map (newPlayerMessage players') players' ++ [nightFallsMessage] ++ turnMessages turn' players'     where         turn'       = game ^. turn         players'    = game ^. players -playersInGameMessage :: [Player] -> Message-playersInGameMessage players = publicMessage $ T.concat [+newPlayersInGameMessage :: [Player] -> Message+newPlayersInGameMessage players = publicMessage $ T.concat [     "A new game of werewolf is starting with ",     T.intercalate ", " (map _name players), "!"     ] -rolesInGameMessage :: [Role] -> Message-rolesInGameMessage roles = publicMessage $ T.concat [-    "The roles in play are ",-    T.intercalate ", " $ map (\(role, count) ->-        T.concat [role ^. Role.name, " (", T.pack $ show count, ")"])-        roleCounts,-    "."-    ]-    where-        roleCounts = map (\list -> (head list, length list)) (groupSortOn Role._name roles)- newPlayerMessage :: [Player] -> Player -> Message newPlayerMessage players player     | isWerewolf player = privateMessage [player ^. name] $ T.intercalate "\n" [T.concat ["You're a Werewolf", packMessage], player ^. role . description]@@ -152,7 +145,7 @@     where         packMessage             | length (filterWerewolves players) <= 1    = "."-            | otherwise                                 = T.concat [", along with ", T.intercalate "," (map _name $ filterWerewolves players \\ [player]), "."]+            | otherwise                                 = T.concat [", along with ", T.intercalate ", " (map _name $ filterWerewolves players \\ [player]), "."]  turnMessages :: Turn -> [Player] -> [Message] turnMessages Seers players      = seersTurnMessages $ filter isSeer players@@ -170,6 +163,44 @@ playerQuitMessage :: Player -> Message playerQuitMessage player = publicMessage $ T.unwords [player ^. name, "the", player ^. role . Role.name, "has quit!"] +currentTurnMessage :: Text -> Turn -> Message+currentTurnMessage name NoOne   = gameIsOverMessage name+currentTurnMessage name turn    = privateMessage [name] $ T.unwords [+    "It's currently the", T.pack $ show turn, "turn."+    ]++rolesInGameMessage :: Maybe [Text] -> [Role] -> Message+rolesInGameMessage mTo roles = Message mTo $ T.concat [+    "The roles in play are ",+    T.intercalate ", " $ map (\(role, count) ->+        T.concat [role ^. Role.name, " (", T.pack $ show count, ")"])+        roleCounts,+    "."+    ]+    where+        roleCounts = map (\list -> (head list, length list)) (groupSortOn Role._name roles)++playersInGameMessage :: Text -> [Player] -> Message+playersInGameMessage to players = privateMessage [to] . T.intercalate "\n" $ [+    alivePlayersText+    ] ++ if (null $ filterDead players) then [] else [deadPlayersText]+    where+        alivePlayersText = T.concat [+            "The following players are still alive: ",+            T.intercalate ", " (map _name $ filterAlive players), "."+            ]+        deadPlayersText = T.concat [+            "The following players are dead: ",+            T.intercalate ", " (map (\player -> T.concat [player ^. name, " (", player ^. role . Role.name, ")"]) $ filterDead players), "."+            ]++waitingOnMessage :: Text -> [Player] -> Message+waitingOnMessage name players = privateMessage [name] $ T.concat [+    "Waiting on ", T.intercalate ", " playerNames, "..."+    ]+    where+        playerNames = map _name players+ seersTurnMessages :: [Player] -> [Message] seersTurnMessages seers = [     publicMessage "The Seers wake up.",@@ -207,6 +238,13 @@ werewolvesTurnMessages werewolves = [     publicMessage "The Werewolves wake up, recognise one another and choose a new victim.",     privateMessage (map _name werewolves) "Whom would you like to devour?"+    ]++scapegoatLynchedMessage :: Text -> Message+scapegoatLynchedMessage name = publicMessage $ T.unwords [+    "The townsfolk squabble over whom to tie up. Just as they are about to call it a day",+    "they notice that", name, "has been acting awfully suspicious.",+    "Not wanting to take any chances,", name, "is promptly tied to a pyre and burned alive."     ]  playerMadeDevourVoteMessage :: [Text] -> Text -> Text -> Message
src/Game/Werewolf/Role.hs view
@@ -17,7 +17,7 @@     Role(..), name, allegiance, description, advice,      -- ** Instances-    allRoles, seerRole, villagerRole, werewolfRole,+    allRoles, seerRole, villagerRole, werewolfRole, scapegoatRole,      -- ** Queries     findByName, findByName_,@@ -43,7 +43,7 @@     } deriving (Eq, Read, Show)  allRoles :: [Role]-allRoles = [seerRole, villagerRole, werewolfRole]+allRoles = [seerRole, villagerRole, werewolfRole, scapegoatRole]  seerRole :: Role seerRole = Role@@ -76,6 +76,15 @@     , _description  = "A shapeshifting townsperson that, at night, hunts the residents of Millers Hollow."     , _advice       =         "Voting against your partner can be a good way to deflect suspicion from yourself."+    }++scapegoatRole :: Role+scapegoatRole = Role+    { _name         = "Scapegoat"+    , _allegiance   = Villagers+    , _description  = "That one person everyone loves to blame."+    , _advice       =+        "Cross your fingers that the votes don't end up tied."     }  findByName :: Text -> Maybe Role
test/app/Main.hs view
@@ -78,7 +78,8 @@      testProperty "PROP: check villagers turn advances to seers" prop_checkVillagersTurnAdvancesToSeers,     testProperty "PROP: check villagers turn lynches one player when consensus" prop_checkVillagersTurnLynchesOnePlayerWhenConsensus,-    testProperty "PROP: check villagers turn lynches no one when conflicted" prop_checkVillagersTurnLynchesNoOneWhenConflicted,+    testProperty "PROP: check villagers turn lynches no one when conflicted and no scapegoats" prop_checkVillagersTurnLynchesNoOneWhenConflictedAndNoScapegoats,+    testProperty "PROP: check villagers turn lynches scapegoat when conflicted" prop_checkVillagersTurnLynchesScapegoatWhenConflicted,     testProperty "PROP: check villagers turn resets votes" prop_checkVillagersTurnResetsVotes,     testProperty "PROP: check villagers turn does nothing unless all voted" prop_checkVillagersTurnDoesNothingUnlessAllVoted, @@ -89,7 +90,7 @@     testProperty "PROP: check werewolves turn does nothing unless all voted" prop_checkWerewolvesTurnDoesNothingUnlessAllVoted,      testProperty "PROP: check game over advances turn" prop_checkGameOverAdvancesTurn,-    testProperty "PROP: check game over does nothing when at least two allegiances alivevoted" prop_checkGameOverDoesNothingWhenAtLeastTwoAllegiancesAlive,+    testProperty "PROP: check game over does nothing when at least two allegiances alive" prop_checkGameOverDoesNothingWhenAtLeastTwoAllegiancesAlive,      testProperty "PROP: start game starts with seers turn when seers present" prop_startGameStartsWithSeersTurnWhenSeersPresent,     testProperty "PROP: start game starts with werewolves turn when seers absent" prop_startGameStartsWithWerewolvesTurnWhenSeersAbsent,
test/src/Game/Werewolf/Test/Arbitrary.hs view
@@ -12,7 +12,7 @@     -- * Contextual arbitraries     arbitraryCommand, arbitraryDevourVoteCommand, arbitraryLynchVoteCommand, arbitraryQuitCommand,     arbitrarySeeCommand, arbitraryNewGame, arbitraryPlayer, arbitrarySeer, arbitraryVillager,-    arbitraryWerewolf,+    arbitraryWerewolf, arbitraryScapegoat,      -- * Utility functions     run, run_, runArbitraryCommands,@@ -96,9 +96,10 @@      let seer        = head $ filterSeers players     let werewolves  = take (n `quot` 6 + 1) $ filterWerewolves players-    let villagers   = take (n - 1 - (length werewolves)) $ filterVillagers players+    let villagers   = take (n - 2 - (length werewolves)) $ filterVillagers players+    let scapegoat   = head $ filterScapegoats players -    return $ newGame (seer:werewolves ++ villagers)+    return $ newGame (seer:scapegoat:werewolves ++ villagers)  instance Arbitrary Turn where     arbitrary = elements [Seers, Villagers, Werewolves, NoOne]@@ -117,6 +118,9 @@  arbitraryWerewolf :: Game -> Gen Player arbitraryWerewolf = elements . filterAlive . filterWerewolves . _players++arbitraryScapegoat :: Game -> Gen Player+arbitraryScapegoat = elements . filterAlive . filterScapegoats . _players  instance Arbitrary State where     arbitrary = elements [Alive, Dead]
test/src/Game/Werewolf/Test/Engine.hs view
@@ -13,8 +13,8 @@     prop_checkTurnSkipsSeersWhenNoSeers, prop_checkTurnDoesNothingWhenGameOver,     prop_checkSeersTurnAdvancesToWerewolves, prop_checkSeersTurnResetsSees,     prop_checkSeersTurnDoesNothingUnlessAllSeen, prop_checkVillagersTurnAdvancesToSeers,-    prop_checkVillagersTurnLynchesOnePlayerWhenConsensus,-    prop_checkVillagersTurnLynchesNoOneWhenConflicted, prop_checkVillagersTurnResetsVotes,+    prop_checkVillagersTurnLynchesOnePlayerWhenConsensus, prop_checkVillagersTurnLynchesNoOneWhenConflictedAndNoScapegoats,+    prop_checkVillagersTurnLynchesScapegoatWhenConflicted, prop_checkVillagersTurnResetsVotes,     prop_checkVillagersTurnDoesNothingUnlessAllVoted, prop_checkWerewolvesTurnAdvancesToVillagers,     prop_checkWerewolvesTurnKillsOnePlayerWhenConsensus,     prop_checkWerewolvesTurnKillsNoOneWhenConflicted, prop_checkWerewolvesTurnResetsVotes,@@ -113,11 +113,21 @@         game'   = game { _turn = Villagers }         n       = length $ game' ^. players -prop_checkVillagersTurnLynchesNoOneWhenConflicted :: Game -> Property-prop_checkVillagersTurnLynchesNoOneWhenConflicted game =+prop_checkVillagersTurnLynchesNoOneWhenConflictedAndNoScapegoats :: Game -> Property+prop_checkVillagersTurnLynchesNoOneWhenConflictedAndNoScapegoats game =     forAll (runArbitraryCommands n game') $ \game'' ->     length (last $ groupSortOn (length . flip elemIndices (Map.elems $ game'' ^. votes)) (nub . Map.elems $ game'' ^. votes)) > 1-    ==> length (filterDead $ run_ checkTurn game'' ^. players) == 0+    ==> length (filterDead $ run_ checkTurn game'' ^. players) == length (filterDead $ game' ^. players)+    where+        game'   = (foldl killPlayer game (filterScapegoats $ game ^. players)) { _turn = Villagers }+        n       = length $ game' ^. players++prop_checkVillagersTurnLynchesScapegoatWhenConflicted :: Game -> Property+prop_checkVillagersTurnLynchesScapegoatWhenConflicted game =+    forAll (runArbitraryCommands n game') $ \game'' -> and [+        length (last $ groupSortOn (length . flip elemIndices (Map.elems $ game'' ^. votes)) (nub . Map.elems $ game'' ^. votes)) > 1,+        any isScapegoat $ game' ^. players+        ] ==> isScapegoat $ head (filterDead $ run_ checkTurn game'' ^. players)     where         game'   = game { _turn = Villagers }         n       = length $ game' ^. players
werewolf.cabal view
@@ -1,5 +1,5 @@ name:           werewolf-version:        0.3.2.0+version:        0.3.3.0  author:         Henry J. Wylde maintainer:     public@hjwylde.com