Skip to content

Commit

Permalink
Merge pull request #520 from lykoss/vampires
Browse files Browse the repository at this point in the history
Add vampires
  • Loading branch information
Vgr255 authored Oct 19, 2023
2 parents 123b707 + 3dc7f7f commit 1c3fca2
Show file tree
Hide file tree
Showing 51 changed files with 677 additions and 218 deletions.
61 changes: 45 additions & 16 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,12 @@
"sharpshooter": ["sharpshooter", "sharpshooters"],
"sorcerer": ["sorcerer", "sorcerers"],
"succubus": ["succubus", "succubi"],
"thrall": ["thrall", "thralls"],
"time lord": ["time lord", "time lords"],
"tough wolf": ["tough wolf", "tough wolves"],
"turncoat": ["turncoat", "turncoats"],
"traitor": ["traitor", "traitors"],
"turncoat": ["turncoat", "turncoats"],
"vampire": ["vampire", "vampires"],
"vengeful ghost": ["vengeful ghost", "vengeful ghosts"],
"vigilante": ["vigilante", "vigilantes"],
"villager": ["villager", "villagers"],
Expand All @@ -83,10 +85,11 @@
"neutral player": ["neutral player", "neutral players"],
"wolfteam player": ["wolfteam player", "wolfteam players"],
"***": "The following items are special keys used in player stats",
"drained": ["drained", "drained"],
"entranced": ["entranced", "entranced"],
"lover": ["lover", "lovers"],
"vg activated": ["vg activated", "vgs activated"],
"vg driven off": ["vg driven off", "vgs driven off"],
"entranced": ["entranced", "entranced"]
"vg driven off": ["vg driven off", "vgs driven off"]
},
"_gamemodes": {
"*": "For assistance in configuring this section, please see https://werewolf.chat/Translation#Gamemodes",
Expand Down Expand Up @@ -275,9 +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 @@ -548,9 +554,12 @@
"piper_win": "Game over! Everyone has fallen victim to the charms of the {=piper!role:plural({0})}. The {=piper!role:plural({0})} {=leads,lead:plural({0})} the villagers away from the village, never to return...",
"monster_win": "Game over! All the wolves are dead! As the villagers start preparing the BBQ, the {=monster!role:plural({0})} quickly {=kills,kill:plural({0})} the remaining villagers, causing the {=monster!role:plural({0})} to win.",
"monster_wolf_win": "Game over! There are the same number of wolves as uninjured villagers. The wolves overpower the villagers but then get destroyed by the {=monster!role:plural({0})}, causing the {=monster!role:plural({0})} to win.",
"monster_vampire_win": "Game over! There are the same number of vampires as uninjured villagers. The vampires overpower the villagers but then get destroyed by the {=monster!role:plural({0})}, causing the {=monster!role:plural({0})} to win.",
"villager_win": "Game over! All the wolves are dead! The villagers chop them up, BBQ them, and have a hearty meal.",
"wolf_win_equal": "Game over! There are the same number of wolves as uninjured villagers. The wolves overpower the villagers and win.",
"wolf_win_greater": "Game over! There are more wolves than uninjured villagers. The wolves overpower the villagers and win.",
"vampire_win_equal": "Game over! There are the same number of vampires as uninjured villagers. The vampires overpower the villagers and win.",
"vampire_win_greater": "Game over! There are more vampires than uninjured villagers. The vampires overpower the villagers and win.",
"new_game": "{0:@} has started a game of Werewolf. Type \"{=join!command:!}\" to join. Type \"{=start!command:!}\" to vote to start the game. Type \"{=wait!command:!}\" to increase the start wait time.",
"you_stasis": "Sorry, but you are in stasis for {0} {=game,games:plural({0})}.",
"other_stasis": "Sorry, but {0} is in stasis for {1} {=game,games:plural({1})}",
Expand Down Expand Up @@ -739,9 +748,7 @@
"command_disabled": "This command has been disabled.",
"exhausted_abstain": "The village has already abstained once this game and may not do so again.",
"no_abstain_day_one": "The village may not abstain on the first day.",
"wounded_absent": "You are wounded and resting, thus you are unable to vote for the day.",
"player_abstain": "{0:@} votes not to kill anyone today.",
"totem_absent": "As you place your vote, your totem emits a brilliant flash of light. After recovering, you notice that you are still in your bed. That entire sequence of events must have just been a dream...",
"no_self_lynch": "You may not vote yourself.",
"player_vote": "{0:@} votes for {1:@}.",
"protector_disappeared": "Your protector seems to have disappeared...",
Expand Down Expand Up @@ -901,6 +908,15 @@
"charm_multiple_success": "You have charmed {0:@} and {1:@}.",
"another_piper_charmed_multiple": "Another piper has charmed {0:@} and {1:@}!",
"another_piper_charmed": "Another piper has charmed {0:@}!",
"no_target_vampire": "You may not bite another vampire.",
"already_bitten_tonight": "{0:@} is already being bitten by another vampire tonight; you must select a different target.",
"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 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.",
"privmsg_idle_warning": "[b]You have been idling in {0} for a while. Please say something in {0} or you will be declared dead.[/b]",
"day_lasted": "Day lasted [b]{0:0>2}:{1:0>2}[/b].",
"fallen_angel_turn": "While out last night, you were overpowered by a large werewolf and bitten. Shortly thereafter, you found your wings turning black as night and sadistic thoughts infiltrating your mind...",
Expand All @@ -925,16 +941,20 @@
"wolf_gunner_notify": "You are {=wolf gunner!role:article} {=wolf gunner!role:bold} and hold a gun. You may only use it during the day by typing \"{=shoot!command:!} <nick>\" in channel. If you shoot at a wolf, you will intentionally miss. If you shoot a villager, it is likely that they will die. You may also use \"{=kill!command} <nick>\" during the night to kill a villager.",
"tough_wolf_notify": "You are {=tough wolf!role:article} {=tough wolf!role:bold}. Your hide is strong enough to survive attacks from the village mob once per game, allowing you to escape alive. Also, the {=gunner!role} is unable to kill you with their bullets. Use \"{=kill!command} <nick>\" to kill a villager.",
"undefined_role_notify": "You are {0:article} {0:bold}. There would normally be instructions here, but someone forgot to add them in. Please report this to the admins, you can PM me \"{=admins!command}\" for a list of available ones.",
"wolfchat_notify_1": "Also, if you PM me, your message will be relayed to other wolves.",
"wolfchat_notify_2": "Also, if you PM me during the day, your message will be relayed to other wolves.",
"wolfchat_notify_3": "Also, if you PM me during the night, your message will be relayed to other wolves.",
"wolfchat_notify_1": "Also, if you PM me, your message will be relayed to other {0!cat:plural}.",
"wolfchat_notify_2": "Also, if you PM me during the day, your message will be relayed to other {0!cat:plural}.",
"wolfchat_notify_3": "Also, if you PM me during the night, your message will be relayed to other {0!cat:plural}.",
"relay_action": "* {0:@} {1}",
"relay_action_wolfchat": "* [[wolfchat]] {0:@} {1}",
"relay_action_vampchat": "* [[vampire chat]] {0:@} {1}",
"relay_action_deadchat": "* [[deadchat]] {0:@} {1}",
"relay_message": "{0:@} says: {1}",
"relay_message_wolfchat": "[[wolfchat]] {0:@} says: {1}",
"relay_message_vampchat": "[[vampire chat]] {0:@} says: {1}",
"relay_message_deadchat": "[[deadchat]] {0:@} says: {1}",
"relay_command_wolfchat": "[[wolfchat]] {0}",
"relay_command_vampchat": "[[vampire chat]] {0}",
"relay_command_deadchat": "[[deadchat]] {0}",
"cursed_notify": "You are [b]{=cursed villager!role}[/b]. You will be seen by the {=seer!role} and {=oracle!role} as being {=wolf!role:article} {=wolf!role} rather than your normal role!",
"seer_info_general": "You are {0!role:article} {0!role:bold}. It is your job to detect the wolves, you may have a vision once per night.",
"seer_info": "Use \"{=see!command} <nick>\" to see the role of a player.",
Expand Down Expand Up @@ -1218,8 +1238,11 @@
"dullahan_die_success_noreveal": "Before dying, {0:@} snaps a whip made of a human spine at {1:@}, killing them.",
"entranced_revert_win": "You are no longer entranced. [b]Your win conditions have reset to normal.[/b]",
"player_sick": "You woke up today not feeling very well, you think it best to stay home for the remainder of the day and night.",
"wounded_absent": "You are wounded and resting, thus you are unable to vote for the day.",
"consecrating_absent": "You are consecrating someone today and cannot participate in the vote.",
"illness_absent": "You are staying home due to your illness and cannot participate in the vote.",
"drained_absent": "You are staying home due to your blood loss and cannot participate in the vote.",
"totem_absent": "As you place your vote, your totem emits a brilliant flash of light. After recovering, you notice that you are still in your bed. That entire sequence of events must have just been a dream...",
"already_blessed": "You have already blessed someone this game.",
"no_bless_self": "You may not bless yourself.",
"blessed_success": "You have given a blessing to {0:@}.",
Expand Down Expand Up @@ -1257,15 +1280,21 @@
"already_up_to_date": "Already up-to-date.",
"admin_fleave_deadchat": "You have forced {0} to leave the deadchat.",
"available_mode_setters_help": "Votes to make a specific game mode more likely. Available game mode setters: {0:join}",
"spectate_help": "Usage: {=spectate!command} <wolfchat> [[on|off]]",
"fspectate_help": "Usage: {=fspectate!command} <wolfchat|deadchat> [[on|off]]",
"spectate_help": "Usage: {=spectate!command} <wolfchat|vampchat> [[on|off]]",
"fspectate_help": "Usage: {=fspectate!command} <wolfchat|vampchat|deadchat> [[on|off]]",
"spectate_restricted": "You may not spectate while playing.",
"spectate_deadchat_disabled": "Deadchat is disabled and may not be spectated.",
"spectate_in_deadchat": "You are currently in deadchat.",
"spectate_on": "You are now spectating {0}.",
"spectate_off": "You are no longer spectating {0}.",
"spectate_notice_user": "{1:@} is now spectating {0}.",
"spectate_notice": "Someone is now spectating {0}.",
"spectate_on_deadchat": "You are now spectating deadchat.",
"spectate_on_wolfchat": "You are now spectating wolfchat.",
"spectate_on_vampchat": "You are now spectating vampire chat.",
"spectate_off_deadchat": "You are no longer spectating deadchat.",
"spectate_off_wolfchat": "You are no longer spectating wolfchat.",
"spectate_off_vampchat": "You are no longer spectating vampire chat.",
"spectate_wolfchat_notice_user": "{1:@} is now spectating wolfchat.",
"spectate_vampchat_notice_user": "{1:@} is now spectating vampire chat.",
"spectate_wolfchat_notice": "Someone is now spectating wolfchat.",
"spectate_vampchat_notice": "Someone is now spectating vampire chat.",
"stop_bot_ingame_safeguard": "Warning: A game is currently running. If you want to {what} the bot anyway, use \"{cmd:!} -force\".",
"invalid_restart_mode": "{0:bold} is not a valid mode. Valid modes are: {1:join}",
"whoami_loggedin": "You are logged into the account {0:bold}.",
Expand Down
64 changes: 53 additions & 11 deletions src/cats.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# Wolf: Defines the role as a true wolf role (usually can kill, dies when shot, kills visiting harlots, etc.)
# Wolfchat: Defines the role as having access to wolfchat (depending on gameplay.wolfchat config settings)
# Wolfteam: Defines the role as wolfteam for determining winners
# Vampire: Defines the role as a true vampire role (usually able to bite, access to vampire chat)
# Vampire Team: Defines the role as vampire team for determining winners
# Killer: Roles which can kill other roles during the game. Roles which kill upon or after death (ms, vg) don't belong in here
# Village: Defines the role as village for determining winners
# Nocturnal: Defines the role as being awake at night (usually due to having commands which work at night)
Expand All @@ -16,22 +18,29 @@
# Innocent: Seer sees these roles as the default role even if they would otherwise be seen as wolf
# Team Switcher: Roles which may change teams during gameplay
# Wolf Objective: If the number of alive players with this role is greater than or equal to the other players,
# the wolfteam wins. Only main roles are considered for this.
# the wolfteam wins. Only main roles are considered for this. All Vampire Objectives must additionally be dead.
# Vampire Objective: If the number of alive players with this role is greater than or equal to the other players,
# the vampire team wins. Only main roles are considered for this. All Wolf Objectives must additionally be dead.
# Village Objective: If all of the players with this cat are dead, the village wins.
# Only main roles are considered for this.

from __future__ import annotations

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", "All"
"Spy", "Intuitive", "Cursed", "Innocent", "Team_Switcher", "Wolf_Objective", "Village_Objective",
"Vampire", "Vampire_Team", "Vampire_Objective", "All"
]

_dict_keys = type(dict().keys()) # type: ignore
Expand All @@ -42,11 +51,12 @@
# the ordering in which we list roles (values should be categories, and roles are ordered within the categories in alphabetical order,
# with exception that wolf is first in the wolf category and villager is last in the village category)
# Roles which are always secondary roles in a particular game mode are always listed last (after everything else is done)
ROLE_ORDER = ["Wolf", "Wolfchat", "Wolfteam", "Village", "Hidden", "Win Stealer", "Neutral"]
ROLE_ORDER = ["Wolf", "Wolfchat", "Wolfteam", "Vampire", "Vampire Team", "Village", "Hidden", "Win Stealer", "Neutral"]

FROZEN = False

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

_internal_en = _Messages(override="en")

Expand All @@ -66,12 +76,14 @@ def role_order():
if tag in tags:
buckets[tag].append(role)
break
# handle fixed ordering for wolf and villager
# handle fixed ordering for wolf, vampire, and villager
buckets["Wolf"].remove("wolf")
buckets["Vampire"].remove("vampire")
buckets["Village"].remove("villager")
for tags in buckets.values():
tags.sort()
buckets["Wolf"].insert(0, "wolf")
buckets["Vampire"].insert(0, "vampire")
buckets["Village"].append("villager")
return itertools.chain.from_iterable([buckets[tag] for tag in ROLE_ORDER])

Expand All @@ -94,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", "Village", "Neutral", "Hidden"}) != 1:
raise RuntimeError("Invalid categories for {0}: Must have exactly one of {{Wolfteam, 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 @@ -112,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 @@ -176,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 @@ -222,6 +260,8 @@ def from_combination(cls, first, second, op, func):
Wolf = Category("Wolf")
Wolfchat = Category("Wolfchat")
Wolfteam = Category("Wolfteam")
Vampire = Category("Vampire")
Vampire_Team = Category("Vampire Team")
Killer = Category("Killer")
Village = Category("Village")
Nocturnal = Category("Nocturnal")
Expand All @@ -236,3 +276,5 @@ def from_combination(cls, first, second, op, func):
Team_Switcher = Category("Team Switcher")
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/defaultsettings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -782,7 +782,7 @@ gameplay: &gameplay
_desc: >
Many wolf roles have access to wolfchat, which allows them to communicate with each other throughout the
night and day in private through the bot. This section allows for implementing restrictions on when wolfchat
may be used and who is able to use it.
may be used and who is able to use it. These settings also apply to vampire chat, where sensible.
_type: dict
_default:
disable_day:
Expand Down
Loading

0 comments on commit 1c3fca2

Please sign in to comment.