Skip to content

Commit

Permalink
More vampire updates
Browse files Browse the repository at this point in the history
- Define a new Evil role category for use by mystic that includes wolves
  and vampires
- Rename the en messages for the various "team" categories as these
  names are now used to match winners
- More generic team handling to allow for modification of which teams
  are in the game
- Investigator and Augur are now more generic (looking for generic team
  or Evil, respectively)
- Let vigilantes kill vampires
- Let wolf mystic see vampire count
- Hide vampires/win stealers from wolf mystic if those aren't in the
  game
  • Loading branch information
skizzerz committed Oct 18, 2023
1 parent 5ae3ca0 commit 3dc7f7f
Show file tree
Hide file tree
Showing 30 changed files with 86 additions and 71 deletions.
8 changes: 5 additions & 3 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -278,10 +278,12 @@
"*": "For assistance in configuring this metadata, please see https://werewolf.chat/Translation#Role_categories",
"Wolf": ["wolf", "wolves"],
"Wolfchat": ["wolfchat member", "wolfchat members"],
"Wolfteam": ["evil villager", "evil villagers"],
"Wolfteam": ["wolf", "wolves"],
"Evil": ["evil villager", "evil villagers"],
"Vampire": ["vampire", "vampires"],
"Vampire Team": ["vampire", "vampires"],
"Killer": ["killer", "killers"],
"Village": ["village member", "village members"],
"Village": ["villager", "villagers"],
"Nocturnal": ["nocturnal role", "nocturnal roles"],
"Neutral": ["neutral players", "neutral players"],
"Win Stealer": ["win stealer", "win stealers"],
Expand Down Expand Up @@ -911,7 +913,7 @@
"vampire_bite": "You have chosen to bite {0:@} tonight.",
"vampire_bite_vampchat": "{0:@} has chosen to bite {1:@} tonight.",
"retracted_bite": "You have retracted your bite.",
"retracted_bite_vampchat": "{0:@} has retracted your bite.",
"retracted_bite_vampchat": "{0:@} has retracted their bite.",
"vampire_drained": "You woke suddenly last night to a sharp pain in your neck, it seems a vampire drained your blood! You weren't able to get a good look at them, and the blood loss makes you feel lightheaded and unable to do anything.",
"vampire_notify": "You are {=vampire!role:article} {=vampire!role:bold}. It is your job to kill all the villagers. Use \"{=bite!command} <nick>\" to drain the blood from a villager. If they were already drained, this will kill them.",
"thrall_notify": "You are {=thrall!role:article} {=thrall!role:bold}. It is your job to help the vampires take over the village.",
Expand Down
46 changes: 39 additions & 7 deletions src/cats.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,16 @@

from collections import defaultdict
import itertools
import typing

from src.messages._messages import Messages as _Messages
from src.events import Event, EventListener

if typing.TYPE_CHECKING:
from src.gamestate import GameState

__all__ = [
"get", "role_order", "all_cats", "all_roles", "Category",
"get", "get_team", "role_order", "all_cats", "all_roles", "Category",
"Wolf", "Wolfchat", "Wolfteam", "Killer", "Village", "Nocturnal", "Neutral", "Win_Stealer", "Hidden", "Safe",
"Spy", "Intuitive", "Cursed", "Innocent", "Team_Switcher", "Wolf_Objective", "Village_Objective",
"Vampire", "Vampire_Team", "Vampire_Objective", "All"
Expand All @@ -52,6 +56,7 @@
FROZEN = False

ROLES = {}
TEAMS: set[Category] = set()

_internal_en = _Messages(override="en")

Expand Down Expand Up @@ -101,13 +106,31 @@ def all_roles() -> dict[str, list[Category]]:
roles[role] = [next(iter(main_cat))] + sorted(iter(cats), key=str)
return roles

def get_team(var: GameState, role: str) -> Category:
if not FROZEN:
raise RuntimeError("Fatal: Role categories are not ready")
if Hidden in TEAMS and role in Hidden:
role = var.hidden_role
for team in TEAMS:
if role in team:
return team

def _register_roles(evt: Event):
global FROZEN
mevt = Event("get_role_metadata", {})
mevt.dispatch(None, "role_categories")
for role, cats in mevt.data.items():
if len(cats & {"Wolfteam", "Vampire Team", "Village", "Neutral", "Hidden"}) != 1:
raise RuntimeError("Invalid categories for {0}: Must have exactly one of {{Wolfteam, Vampire Team, Village, Neutral, Hidden}}, got {1}".format(role, cats))
team_evt = Event("get_role_metadata", {
"teams": {"Wolfteam", "Vampire Team", "Village", "Neutral", "Hidden"}
})
team_evt.dispatch(None, "team_categories")
teams = set(team_evt.data["teams"])
for cat in teams:
if cat not in ROLE_CATS or ROLE_CATS[cat] is All:
raise ValueError("{0!r} is not a valid role category".format(cat))

evt = Event("get_role_metadata", {})
evt.dispatch(None, "role_categories")
for role, cats in evt.data.items():
if len(cats & teams) != 1:
raise RuntimeError("Invalid categories for {0}: Must have exactly one team defined".format(role))
ROLES[role] = frozenset(cats)
for cat in cats:
if cat not in ROLE_CATS or ROLE_CATS[cat] is All:
Expand All @@ -119,6 +142,9 @@ def _register_roles(evt: Event):
cat.freeze()
FROZEN = True

for cat in teams:
TEAMS.add(ROLE_CATS[cat])

EventListener(_register_roles, priority=1).install("init")

class Category:
Expand Down Expand Up @@ -183,13 +209,18 @@ def __str__(self):
def __repr__(self):
return "Role category: {0}".format(self.name)

def plural(self):
def plural_roles(self):
"""Return the English plural versions of roles for internal use."""
values = set()
for role in self:
values.add(_internal_en.raw("_roles", role)[1])
return values

@property
def plural_name(self):
"""Return the English plural version of this category's name for internal use."""
return _internal_en.raw("_role_categories", self.name)[1]

def __invert__(self):
new = self.from_combination(All, self, "", set.difference_update)
if self.name in ROLE_CATS:
Expand Down Expand Up @@ -246,3 +277,4 @@ def from_combination(cls, first, second, op, func):
Village_Objective = Category("Village Objective")
Wolf_Objective = Category("Wolf Objective")
Vampire_Objective = Category("Vampire Objective")
Evil = Category("Evil")
2 changes: 1 addition & 1 deletion src/roles/alphawolf.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,4 @@ def on_get_role_metadata(evt: Event, var: Optional[GameState], kind: str):
can_bite = get_all_players(var, ("alpha wolf",)) - ALPHAS
evt.data["alpha wolf"] = len(can_bite)
elif kind == "role_categories":
evt.data["alpha wolf"] = {"Wolf", "Wolfchat", "Wolfteam", "Killer", "Nocturnal", "Village Objective", "Wolf Objective"}
evt.data["alpha wolf"] = {"Wolf", "Wolfchat", "Wolfteam", "Killer", "Nocturnal", "Village Objective", "Wolf Objective", "Evil"}
4 changes: 2 additions & 2 deletions src/roles/augur.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import re
from typing import Optional

from src.cats import Neutral, Wolfteam
from src.cats import Neutral, Evil
from src.decorators import command
from src.dispatcher import MessageDispatcher
from src.events import Event, event_listener
Expand Down Expand Up @@ -40,7 +40,7 @@ def see(wrapper: MessageDispatcher, message: str):
targrole = evt.data["role"]

aura = "blue"
if targrole in Wolfteam:
if targrole in Evil:
aura = "red"
elif targrole in Neutral:
aura = "grey"
Expand Down
2 changes: 1 addition & 1 deletion src/roles/cultist.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@ def on_chk_win(evt: Event, var: GameState, rolemap: dict[str, set[User]], mainro
@event_listener("get_role_metadata")
def on_get_role_metadata(evt: Event, var: Optional[GameState], kind: str):
if kind == "role_categories":
evt.data["cultist"] = {"Wolfteam"}
evt.data["cultist"] = {"Wolfteam", "Evil"}
2 changes: 1 addition & 1 deletion src/roles/cultleader.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@
@event_listener("get_role_metadata")
def on_get_role_metadata(evt: Event, var: Optional[GameState], kind: str):
if kind == "role_categories":
evt.data["cult leader"] = {"Wolfchat", "Wolfteam"}
evt.data["cult leader"] = {"Wolfchat", "Wolfteam", "Evil"}
2 changes: 1 addition & 1 deletion src/roles/detective.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def investigate(wrapper: MessageDispatcher, message: str):
wrapper.send(messages["investigate_success"].format(target, targrole))

if random.randrange(0, 100) < config.Main.get("gameplay.safes.detective_reveal"): # a 2/5 chance (changeable in settings)
# The detective's identity is compromised! Let the opposing team know
# The detective's identity is compromised! Let the wolves know
if get_main_role(var, wrapper.source) in Wolfteam:
to_notify = get_players(var, Safe)
else:
Expand Down
2 changes: 1 addition & 1 deletion src/roles/doomsayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,4 @@ def on_reset(evt: Event, var: GameState):
@event_listener("get_role_metadata")
def on_get_role_metadata(evt: Event, var: Optional[GameState], kind: str):
if kind == "role_categories":
evt.data["doomsayer"] = {"Wolf", "Wolfchat", "Wolfteam", "Killer", "Nocturnal", "Village Objective", "Wolf Objective"}
evt.data["doomsayer"] = {"Wolf", "Wolfchat", "Wolfteam", "Killer", "Nocturnal", "Village Objective", "Wolf Objective", "Evil"}
2 changes: 1 addition & 1 deletion src/roles/fallenangel.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ def on_try_protection(evt: Event, var: GameState, target: User, attacker: User,
@event_listener("get_role_metadata")
def on_get_role_metadata(evt: Event, var: Optional[GameState], kind: str):
if kind == "role_categories":
evt.data["fallen angel"] = {"Wolf", "Wolfchat", "Wolfteam", "Killer", "Nocturnal", "Village Objective", "Wolf Objective"}
evt.data["fallen angel"] = {"Wolf", "Wolfchat", "Wolfteam", "Killer", "Nocturnal", "Village Objective", "Wolf Objective", "Evil"}
2 changes: 1 addition & 1 deletion src/roles/hag.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,4 @@ def on_reset(evt: Event, var: GameState):
@event_listener("get_role_metadata")
def on_get_role_metadata(evt: Event, var: Optional[GameState], kind: str):
if kind == "role_categories":
evt.data["hag"] = {"Wolfchat", "Wolfteam", "Nocturnal", "Wolf Objective"}
evt.data["hag"] = {"Wolfchat", "Wolfteam", "Nocturnal", "Wolf Objective", "Evil"}
7 changes: 6 additions & 1 deletion src/roles/helper/mystics.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,13 @@ def register_mystic(rolename: str, *, send_role: bool, types: Iterable[str]):
def on_send_role(evt: Event, var: GameState):
values = []

for t in types:
for i, t in enumerate(types):
# if the game didn't start with any of this type of role, hide it from the output
# for safety, we'll always display the first type listed even if there aren't any of that type
cat = cats.get(t)
orig_players = get_players(var, cat, mainroles=var.original_main_roles)
if i > 0 and not orig_players:
continue
players = get_players(var, cat)
values.append((len(players), t))

Expand Down
22 changes: 3 additions & 19 deletions src/roles/investigator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import re
from typing import Optional

from src.cats import Neutral, Wolfteam
from src.cats import get_team
from src.containers import UserSet
from src.decorators import command
from src.dispatcher import MessageDispatcher
Expand Down Expand Up @@ -55,24 +55,8 @@ def investigate(wrapper: MessageDispatcher, message: str):
evt.dispatch(var, wrapper.source, target2, "investigator")
t2role = evt.data["role"]

# FIXME: make a standardized way of getting team affiliation, and make
# augur and investigator both use it (and make it events-aware so other
# teams can be added more easily)
if t1role in Wolfteam:
t1role = "red"
elif t1role in Neutral:
t1role = "grey"
else:
t1role = "blue"

if t2role in Wolfteam:
t2role = "red"
elif t2role in Neutral:
t2role = "grey"
else:
t2role = "blue"

evt = Event("get_team_affiliation", {"same": (t1role == t2role)})
same = get_team(var, t1role) is get_team(var, t2role)
evt = Event("get_team_affiliation", {"same": same})
evt.dispatch(var, target1, target2)

if evt.data["same"]:
Expand Down
2 changes: 1 addition & 1 deletion src/roles/minion.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,4 @@ def on_reset(evt: Event, var: GameState):
@event_listener("get_role_metadata")
def on_get_role_metadata(evt: Event, var: Optional[GameState], kind: str):
if kind == "role_categories":
evt.data["minion"] = {"Wolfteam", "Intuitive"}
evt.data["minion"] = {"Wolfteam", "Intuitive", "Evil"}
2 changes: 1 addition & 1 deletion src/roles/mystic.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from src.gamestate import GameState
from src.roles.helper.mystics import register_mystic

register_mystic("mystic", send_role=True, types=("Wolfteam",))
register_mystic("mystic", send_role=True, types=("Evil",))

@event_listener("get_role_metadata")
def on_get_role_metadata(evt: Event, var: Optional[GameState], kind: str):
Expand Down
1 change: 0 additions & 1 deletion src/roles/seer.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ def see(wrapper: MessageDispatcher, message: str):
return

targrole = get_main_role(var, target)
trole = targrole # keep a copy for logging

if targrole in Cursed:
targrole = "wolf"
Expand Down
2 changes: 1 addition & 1 deletion src/roles/sorcerer.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,4 @@ def on_reset(evt: Event, var: GameState):
@event_listener("get_role_metadata")
def on_get_role_metadata(evt: Event, var: Optional[GameState], kind: str):
if kind == "role_categories":
evt.data["sorcerer"] = {"Wolfchat", "Wolfteam", "Nocturnal", "Spy", "Wolf Objective"}
evt.data["sorcerer"] = {"Wolfchat", "Wolfteam", "Nocturnal", "Spy", "Wolf Objective", "Evil"}
2 changes: 1 addition & 1 deletion src/roles/thrall.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,4 @@ def on_chk_win(evt: Event,
@event_listener("get_role_metadata")
def on_get_role_metadata(evt: Event, var: Optional[GameState], kind: str):
if kind == "role_categories":
evt.data["thrall"] = {"Vampire Team"}
evt.data["thrall"] = {"Vampire Team", "Evil"}
2 changes: 1 addition & 1 deletion src/roles/toughwolf.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ def on_gun_shoot(evt: Event, var: GameState, user: User, target: User, role: str
@event_listener("get_role_metadata")
def on_get_role_metadata(evt: Event, var: Optional[GameState], kind: str):
if kind == "role_categories":
evt.data["tough wolf"] = {"Wolf", "Wolfchat", "Wolfteam", "Killer", "Nocturnal", "Village Objective", "Wolf Objective"}
evt.data["tough wolf"] = {"Wolf", "Wolfchat", "Wolfteam", "Killer", "Nocturnal", "Village Objective", "Wolf Objective", "Evil"}
2 changes: 1 addition & 1 deletion src/roles/traitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,4 @@ def on_chk_win(evt: Event, var: GameState, rolemap: dict[str, set[User]], mainro
@event_listener("get_role_metadata")
def on_get_role_metadata(evt: Event, var: Optional[GameState], kind: str):
if kind == "role_categories":
evt.data["traitor"] = {"Wolfchat", "Wolfteam", "Wolf Objective"}
evt.data["traitor"] = {"Wolfchat", "Wolfteam", "Wolf Objective", "Evil"}
2 changes: 1 addition & 1 deletion src/roles/vampire.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ def on_get_role_metadata(evt: Event, var: Optional[GameState], kind: str):
if kind == "night_kills":
evt.data["vampire"] = min(len(var.vampire_drained), len(get_all_players(var, ("vampire",))))
elif kind == "role_categories":
evt.data["vampire"] = {"Vampire", "Vampire Team", "Killer", "Nocturnal", "Vampire Objective", "Village Objective"}
evt.data["vampire"] = {"Vampire", "Vampire Team", "Killer", "Nocturnal", "Vampire Objective", "Village Objective", "Evil"}

_bite_cmds = ("bite", "retract")

Expand Down
4 changes: 2 additions & 2 deletions src/roles/vigilante.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from typing import Optional

from src import users
from src.cats import Wolf, Win_Stealer
from src.cats import Vampire, Wolf, Win_Stealer
from src.containers import UserSet, UserDict
from src.decorators import command
from src.events import Event, event_listener
Expand Down Expand Up @@ -72,7 +72,7 @@ def on_night_kills(evt: Event, var: GameState):
# important, otherwise our del_player listener instructs vigilante to kill again
del KILLS[vigilante]

if get_main_role(var, target) not in Wolf | Win_Stealer:
if get_main_role(var, target) not in Wolf | Vampire | Win_Stealer:
evt.data["kill_priorities"]["@vigilante"] = 15
evt.data["victims"].add(vigilante)
evt.data["killers"][vigilante].append("@vigilante")
Expand Down
2 changes: 1 addition & 1 deletion src/roles/warlock.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,4 @@ def on_reset(evt: Event, var: GameState):
@event_listener("get_role_metadata")
def on_get_role_metadata(evt: Event, var: Optional[GameState], kind: str):
if kind == "role_categories":
evt.data["warlock"] = {"Wolfchat", "Wolfteam", "Nocturnal", "Wolf Objective"}
evt.data["warlock"] = {"Wolfchat", "Wolfteam", "Nocturnal", "Wolf Objective", "Evil"}
2 changes: 1 addition & 1 deletion src/roles/werecrow.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,4 @@ def on_new_role(evt: Event, var: GameState, player: User, old_role: Optional[str
@event_listener("get_role_metadata")
def on_get_role_metadata(evt: Event, var: Optional[GameState], kind: str):
if kind == "role_categories":
evt.data["werecrow"] = {"Wolf", "Wolfchat", "Wolfteam", "Killer", "Nocturnal", "Spy", "Village Objective", "Wolf Objective"}
evt.data["werecrow"] = {"Wolf", "Wolfchat", "Wolfteam", "Killer", "Nocturnal", "Spy", "Village Objective", "Wolf Objective", "Evil"}
2 changes: 1 addition & 1 deletion src/roles/werekitten.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ def on_gun_shoot(evt: Event, var: GameState, user: User, target: User, role: str
@event_listener("get_role_metadata")
def on_get_role_metadata(evt: Event, var: Optional[GameState], kind: str):
if kind == "role_categories":
evt.data["werekitten"] = {"Wolf", "Wolfchat", "Wolfteam", "Innocent", "Killer", "Nocturnal", "Village Objective", "Wolf Objective"}
evt.data["werekitten"] = {"Wolf", "Wolfchat", "Wolfteam", "Innocent", "Killer", "Nocturnal", "Village Objective", "Wolf Objective", "Evil"}
2 changes: 1 addition & 1 deletion src/roles/wolf.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@
@event_listener("get_role_metadata")
def on_get_role_metadata(evt: Event, var: Optional[GameState], kind: str):
if kind == "role_categories":
evt.data["wolf"] = {"Wolf", "Wolfchat", "Wolfteam", "Killer", "Nocturnal", "Village Objective", "Wolf Objective"}
evt.data["wolf"] = {"Wolf", "Wolfchat", "Wolfteam", "Killer", "Nocturnal", "Village Objective", "Wolf Objective", "Evil"}
2 changes: 1 addition & 1 deletion src/roles/wolfcub.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,4 @@ def on_reset(evt: Event, var: GameState):
@event_listener("get_role_metadata")
def on_get_role_metadata(evt: Event, var: Optional[GameState], kind: str):
if kind == "role_categories":
evt.data["wolf cub"] = {"Wolf", "Wolfchat", "Wolfteam", "Wolf Objective"}
evt.data["wolf cub"] = {"Wolf", "Wolfchat", "Wolfteam", "Wolf Objective", "Evil"}
2 changes: 1 addition & 1 deletion src/roles/wolfgunner.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ def on_gun_shoot(evt: Event, var: GameState, player: User, target: User, role: s
@event_listener("get_role_metadata")
def on_get_role_metadata(evt: Event, var: Optional[GameState], kind: str):
if kind == "role_categories":
evt.data["wolf gunner"] = {"Wolf", "Wolfchat", "Wolfteam", "Killer", "Nocturnal", "Village Objective", "Wolf Objective"}
evt.data["wolf gunner"] = {"Wolf", "Wolfchat", "Wolfteam", "Killer", "Nocturnal", "Village Objective", "Wolf Objective", "Evil"}
4 changes: 2 additions & 2 deletions src/roles/wolfmystic.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
from src.roles.helper.mystics import register_mystic
from src.roles.helper.wolves import register_wolf

register_mystic("wolf mystic", send_role=False, types=("Safe", "Win Stealer"))
register_mystic("wolf mystic", send_role=False, types=("Safe", "Win Stealer", "Vampire Team"))
register_wolf("wolf mystic")

@event_listener("get_role_metadata")
def on_get_role_metadata(evt: Event, var: Optional[GameState], kind: str):
if kind == "role_categories":
evt.data["wolf mystic"] = {"Wolf", "Wolfchat", "Wolfteam", "Killer", "Nocturnal", "Intuitive", "Village Objective", "Wolf Objective"}
evt.data["wolf mystic"] = {"Wolf", "Wolfchat", "Wolfteam", "Killer", "Nocturnal", "Intuitive", "Village Objective", "Wolf Objective", "Evil"}
2 changes: 1 addition & 1 deletion src/roles/wolfshaman.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ def on_transition_night_end(evt: Event, var: GameState):
@event_listener("get_role_metadata")
def on_get_role_metadata(evt: Event, var: Optional[GameState], kind: str):
if kind == "role_categories":
evt.data["wolf shaman"] = {"Wolf", "Wolfchat", "Wolfteam", "Killer", "Nocturnal", "Village Objective", "Wolf Objective"}
evt.data["wolf shaman"] = {"Wolf", "Wolfchat", "Wolfteam", "Killer", "Nocturnal", "Village Objective", "Wolf Objective", "Evil"}

@event_listener("default_totems")
def set_wolf_totems(evt: Event, chances: dict[str, dict[str, int]]):
Expand Down
Loading

0 comments on commit 3dc7f7f

Please sign in to comment.