banned/src/roles/wolf.py
Em Barry ca684ce2e4
Convert wolf.py and friends to the new User API (#347)
Also add a _wolf_helper.py file under the src/roles subfolder, containing helpers for all wolf-related roles.
2018-07-05 15:28:30 -04:00

411 lines
16 KiB
Python

import re
import random
from collections import defaultdict
import src.settings as var
from src.utilities import *
from src.functions import get_players, get_all_players, get_main_role, get_all_roles, get_target
from src import debuglog, errlog, plog, users, channels
from src.decorators import command, event_listener
from src.containers import UserList, UserSet, UserDict, DefaultUserDict
from src.messages import messages
from src.events import Event
from src.roles._wolf_helper import CAN_KILL, is_known_wolf_ally, send_wolfchat_message
KILLS = UserDict() # type: Dict[users.User, List[users.User]]
# wolves able to use the !kill command, roles should add to this in their own files via
# from src.roles import wolf
# wolf.CAN_KILL.add("wolf sphere") # or whatever the new wolf role is
# simply modifying var.WOLF_ROLES will *not* update this!
# TODO: Move this into something else
CAN_KILL.update(var.WOLF_ROLES)
@command("kill", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=CAN_KILL)
def wolf_kill(var, wrapper, message):
"""Kills one or more players as a wolf."""
role = get_main_role(wrapper.source)
# eventually cub will listen on targeted_command and block kills that way
if var.DISEASED_WOLVES:
wrapper.pm(messages["ill_wolves"])
return
pieces = re.split(" +", message)
targets = []
orig = []
nevt = Event("wolf_numkills", {"numkills": 1})
nevt.dispatch(var)
num_kills = nevt.data["numkills"]
if len(pieces) < num_kills:
wrapper.pm(messages["wolf_must_target_multiple"])
return
targs = iter(pieces) # allow random words to follow the initial line without issue
for i in range(num_kills):
target = get_target(var, wrapper, next(targs, None), not_self_message="no_suicide")
if target is None:
return
if is_known_wolf_ally(var, wrapper.source, target):
wrapper.pm(messages["wolf_no_target_wolf"])
return
if target in orig:
wrapper.pm(messages["wolf_must_target_multiple"])
return
orig.append(target)
evt = Event("targeted_command", {"target": target, "misdirection": True, "exchange": True})
if not evt.dispatch(var, wrapper.source, target):
return
target = evt.data["target"]
targets.append(target)
KILLS[wrapper.source] = UserList(targets)
if len(orig) > 1:
# TODO: Expand this so we can support arbitrarily many kills (instead of just one or two)
wrapper.pm(messages["player_kill_multiple"].format(*orig))
msg = messages["wolfchat_kill_multiple"].format(wrapper.source, *orig)
debuglog("{0} ({1}) KILL: {2} ({4}) and {3} ({5})".format(wrapper.source, role, *targets, get_main_role(targets[0]), get_main_role(targets[1])))
else:
wrapper.pm(messages["player_kill"].format(orig[0]))
msg = messages["wolfchat_kill"].format(wrapper.source, orig[0])
debuglog("{0} ({1}) KILL: {2} ({3})".format(wrapper.source, role, targets[0], get_main_role(targets[0])))
send_wolfchat_message(var, wrapper.source, msg, var.WOLF_ROLES, role=role, command="kill")
@command("retract", "r", chan=False, pm=True, playing=True, phases=("night",))
def wolf_retract(var, wrapper, message):
"""Removes a wolf's kill selection."""
if wrapper.source in KILLS:
del KILLS[wrapper.source]
wrapper.pm(messages["retracted_kill"])
send_wolfchat_message(var, wrapper.source, messages["wolfchat_retracted_kill"].format(wrapper.source), var.WOLF_ROLES, role=get_main_role(wrapper.source), command="retract")
if wrapper.source in var.ROLES["alpha wolf"] and wrapper.source.nick in var.BITE_PREFERENCES: # FIXME: Split into alpha wolf and convert to users
del var.BITE_PREFERENCES[wrapper.source.nick]
var.ALPHA_WOLVES.remove(wrapper.source.nick)
wrapper.pm(messages["no_bite"])
send_wolfchat_message(var, wrapper.source, messages["wolfchat_no_bite"].format(wrapper.source), ("alpha wolf",), role="alpha wolf", command="retract")
@event_listener("del_player")
def on_del_player(evt, var, user, mainrole, allroles, death_triggers):
if death_triggers:
# TODO: split into alpha
if allroles & var.WOLF_ROLES:
var.ALPHA_ENABLED = True
for killer, targets in list(KILLS.items()):
for target in targets:
if user is target:
targets.remove(target)
if user is killer or not targets:
del KILLS[killer]
@event_listener("night_acted")
def on_acted(evt, var, user, actor):
if user in KILLS:
evt.data["acted"] = True
@event_listener("get_special")
def on_get_special(evt, var):
evt.data["wolves"].update(get_players(var.WOLFTEAM_ROLES))
@event_listener("transition_day", priority=1)
def on_transition_day(evt, var):
# figure out wolf target
found = defaultdict(int)
nevt = Event("wolf_numkills", {"numkills": 1})
nevt.dispatch(var)
num_kills = nevt.data["numkills"]
for v in KILLS.values():
for p in v:
found[p] += 1
for i in range(num_kills):
maxc = 0
dups = []
for v, c in found.items():
if c > maxc:
maxc = c
dups = [v]
elif c == maxc:
dups.append(v)
if maxc and dups:
target = random.choice(dups)
evt.data["victims"].append(target)
evt.data["bywolves"].add(target)
evt.data["onlybywolves"].add(target)
# special key to let us know to randomly select a wolf in case of retribution totem
evt.data["killers"][target].append("@wolves")
del found[target]
# when monster is split, add protection to them if in onlybywolves
# fallen angel will then remove that protection
# TODO: when monster is split off
if var.ROLES["fallen angel"]:
for monster in get_all_players(("monster",)):
if monster in evt.data["victims"]:
evt.data["victims"].remove(monster)
evt.data["bywolves"].discard(monster)
evt.data["onlybywolves"].discard(monster)
@event_listener("transition_day", priority=3)
def on_transition_day3(evt, var):
evt.data["numkills"] = {v: evt.data["victims"].count(v) for v in set(evt.data["victims"])}
on_transition_day6(evt, var)
@event_listener("transition_day", priority=6)
def on_transition_day6(evt, var):
wolfteam = get_players(var.WOLFTEAM_ROLES)
for victim, killers in list(evt.data["killers"].items()):
k2 = []
kappend = []
wolves = False
for k in killers:
if k in wolfteam:
kappend.append(k)
elif k == "@wolves":
wolves = True
else:
k2.append(k)
k2.extend(kappend)
if wolves:
k2.append("@wolves")
evt.data["killers"][victim] = k2
@event_listener("retribution_kill")
def on_retribution_kill(evt, var, victim, orig_target):
if evt.data["target"] == "@wolves":
wolves = get_players(CAN_KILL)
evt.data["target"] = random.choice(wolves)
@event_listener("new_role", priority=4)
def on_new_role(evt, var, player, old_role):
wcroles = var.WOLFCHAT_ROLES
if var.RESTRICT_WOLFCHAT & var.RW_REM_NON_WOLVES:
if var.RESTRICT_WOLFCHAT & var.RW_TRAITOR_NON_WOLF:
wcroles = var.WOLF_ROLES
else:
wcroles = var.WOLF_ROLES | {"traitor"}
if old_role is None:
# initial role assignment; don't do all the logic below about notifying other wolves and such
if evt.data["role"] in wcroles:
evt.data["in_wolfchat"] = True
return
sayrole = evt.data["role"]
if sayrole in var.HIDDEN_VILLAGERS:
sayrole = "villager"
elif sayrole in var.HIDDEN_ROLES:
sayrole = var.DEFAULT_ROLE
an = "n" if sayrole.startswith(("a", "e", "i", "o", "u")) else ""
if player in KILLS:
del KILLS[player]
if old_role not in wcroles and evt.data["role"] in wcroles:
# a new wofl has joined the party, give them tummy rubs and the wolf list
# and let the other wolves know to break out the confetti and villager steaks
wofls = get_players(wcroles)
evt.data["in_wolfchat"] = True
if wofls:
new_wolves = []
for wofl in wofls:
wofl.queue_message(messages["wolfchat_new_member"].format(player, an, sayrole))
wofl.send_messages()
pl = get_players()
pl.remove(player)
random.shuffle(pl)
pt = []
wevt = Event("wolflist", {"tags": set()})
for p in pl:
prole = get_main_role(p)
wevt.data["tags"].clear()
wevt.dispatch(var, p, player)
tags = " ".join(wevt.data["tags"])
if prole in wcroles:
if tags:
tags += " "
pt.append("\u0002{0}\u0002 ({1}{2})".format(p, tags, prole))
elif tags:
pt.append("{0} ({1})".format(p, tags))
else:
pt.append(p.nick)
evt.data["messages"].append(messages["players_list"].format(", ".join(pt)))
if var.PHASE == "night":
# inform the new wolf that they can kill and stuff
if evt.data["role"] in CAN_KILL and var.DISEASED_WOLVES:
evt.data["messages"].append(messages["ill_wolves"])
# FIXME: split when alpha wolf is split
if var.ALPHA_ENABLED and evt.data["role"] == "alpha wolf" and player.nick not in var.ALPHA_WOLVES:
evt.data["messages"].append(messages["wolf_bite"])
@event_listener("chk_nightdone", priority=3)
def on_chk_nightdone(evt, var):
nevt = Event("wolf_numkills", {"numkills": 1})
nevt.dispatch(var)
num_kills = nevt.data["numkills"]
wofls = [x for x in get_players(CAN_KILL) if x.nick not in var.SILENCED]
if var.DISEASED_WOLVES or num_kills == 0 or len(wofls) == 0:
return
evt.data["nightroles"].extend(wofls)
evt.data["actedcount"] += len(KILLS)
evt.data["nightroles"].append(users.FakeUser.from_nick("@WolvesAgree@"))
# check if wolves are actually agreeing or not;
# only add to count if they actually agree
# (this is *slighty* less hacky than deducting 1 from actedcount as we did previously)
kills = set()
for ls in KILLS.values():
kills.update(ls)
# check if wolves are actually agreeing
if len(kills) == num_kills:
evt.data["actedcount"] += 1
@event_listener("transition_night_end", priority=2)
def on_transition_night_end(evt, var):
ps = get_players()
wolves = get_players(var.WOLFCHAT_ROLES)
# roles in wolfchat (including those that can only listen in but not speak)
wcroles = var.WOLFCHAT_ROLES
# roles allowed to talk in wolfchat
talkroles = var.WOLFCHAT_ROLES
# condition imposed on talking in wolfchat (only during day/night, or None if talking is disabled)
wccond = ""
if var.RESTRICT_WOLFCHAT & var.RW_DISABLE_NIGHT:
if var.RESTRICT_WOLFCHAT & var.RW_DISABLE_DAY:
wccond = None
else:
wccond = " during day"
elif var.RESTRICT_WOLFCHAT & var.RW_DISABLE_DAY:
wccond = " during night"
if var.RESTRICT_WOLFCHAT & var.RW_REM_NON_WOLVES:
if var.RESTRICT_WOLFCHAT & var.RW_TRAITOR_NON_WOLF:
wcroles = var.WOLF_ROLES
talkroles = var.WOLF_ROLES
else:
wcroles = var.WOLF_ROLES | {"traitor"}
talkroles = var.WOLF_ROLES | {"traitor"}
elif var.RESTRICT_WOLFCHAT & var.RW_WOLVES_ONLY_CHAT:
if var.RESTRICT_WOLFCHAT & var.RW_TRAITOR_NON_WOLF:
talkroles = var.WOLF_ROLES
else:
talkroles = var.WOLF_ROLES | {"traitor"}
for wolf in wolves:
normal_notify = not wolf.prefers_simple()
role = get_main_role(wolf)
wevt = Event("wolflist", {"tags": set()})
tags = ""
if role in wcroles:
wevt.dispatch(var, wolf, wolf)
tags = " ".join(wevt.data["tags"])
if tags:
tags += " "
if normal_notify:
msg = "{0}_notify".format(role.replace(" ", "_"))
cmsg = "cursed_" + msg
if "cursed" in wevt.data["tags"]:
try:
tags2 = " ".join(wevt.data["tags"] - {"cursed"})
if tags2:
tags2 += " "
wolf.send(messages[cmsg].format(tags2))
except KeyError:
wolf.send(messages[msg].format(tags))
else:
wolf.send(messages[msg].format(tags))
if len(wolves) > 1 and wccond is not None and role in talkroles:
wolf.send(messages["wolfchat_notify"].format(wccond))
else:
an = ""
if tags:
if tags.startswith(("a", "e", "i", "o", "u")):
an = "n"
elif role.startswith(("a", "e", "i", "o", "u")):
an = "n"
wolf.send(messages["wolf_simple"].format(an, tags, role)) # !simple
if var.FIRST_NIGHT:
minions = len(get_all_players(("minion",)))
if minions > 0:
wolf.send(messages["has_minions"].format(minions, plural("minion", minions)))
pl = ps[:]
random.shuffle(pl)
pl.remove(wolf) # remove self from list
players = []
if role in wcroles:
for player in pl:
prole = get_main_role(player)
wevt.data["tags"] = set()
wevt.dispatch(var, player, wolf)
tags = " ".join(wevt.data["tags"])
if prole in wcroles:
if tags:
tags += " "
players.append("\u0002{0}\u0002 ({1}{2})".format(player, tags, prole))
elif tags:
players.append("{0} ({1})".format(player, tags))
else:
players.append(player.nick)
elif role == "warlock":
# warlock specifically only sees cursed if they're not in wolfchat
for player in pl:
if player in var.ROLES["cursed villager"]:
players.append(player.nick + " (cursed)")
else:
players.append(player.nick)
wolf.send(messages["players_list"].format(", ".join(players)))
if role in CAN_KILL and var.DISEASED_WOLVES:
wolf.send(messages["ill_wolves"])
# TODO: split the following out into their own files (alpha)
if var.ALPHA_ENABLED and role == "alpha wolf" and wolf.nick not in var.ALPHA_WOLVES: # FIXME: Fix once var.ALPHA_WOLVES holds User instances
wolf.send(messages["wolf_bite"])
@event_listener("begin_day")
def on_begin_day(evt, var):
KILLS.clear()
@event_listener("reset")
def on_reset(evt, var):
KILLS.clear()
@event_listener("get_role_metadata")
def on_get_role_metadata(evt, var, kind):
if kind == "night_kills":
nevt = Event("wolf_numkills", {"numkills": 1})
nevt.dispatch(var)
evt.data["wolf"] = nevt.data["numkills"]
# TODO: split into alpha
if var.ALPHA_ENABLED:
# alpha wolf gives an extra kill; note that we consider someone being
# bitten a "kill" for this metadata kind as well
# rolled into wolf instead of as a separate alpha wolf key for ease of implementing
# special logic for wolf kills vs non-wolf kills (as when alpha kills it is treated
# as any other wolf kill).
evt.data["wolf"] += 1
@event_listener("wolf_numkills", priority=10)
def on_wolf_numkills(evt, var):
if var.DISEASED_WOLVES:
evt.data["numkills"] = 0
evt.stop_processing = True
# vim: set sw=4 expandtab: