banned/src/roles/wolf.py
2018-04-27 15:31:11 -07:00

480 lines
18 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
from src import debuglog, errlog, plog, users, channels
from src.decorators import cmd, event_listener
from src.containers import UserList, UserSet, UserDict, DefaultUserDict
from src.messages import messages
from src.events import Event
KILLS = {} # type: Dict[str, List[str]]
# 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!
CAN_KILL = set(var.WOLF_ROLES) # type: Set[str]
def wolf_can_kill(var, wolf):
# a wolf can kill if wolves in general can kill, and the wolf belongs to a role in CAN_KILL
# this is a utility function meant to be used by other wolf role modules
nevt = Event("wolf_numkills", {"numkills": 1})
nevt.dispatch(var)
num_kills = nevt.data["numkills"]
if num_kills == 0:
return False
wolfroles = get_all_roles(wolf)
return bool(CAN_KILL & wolfroles)
@cmd("kill", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=CAN_KILL)
def wolf_kill(cli, nick, chan, rest):
"""Kills one or more players as a wolf."""
role = get_role(nick)
# eventually cub will listen on targeted_command and block kills that way
if var.DISEASED_WOLVES:
pm(cli, nick, messages["ill_wolves"])
return
pieces = re.split(" +", rest)
victims = []
orig = []
nevt = Event("wolf_numkills", {"numkills": 1})
nevt.dispatch(var)
num_kills = nevt.data["numkills"]
wolf = users._get(nick) # FIXME
i = 0
extra = 0
while i < num_kills + extra:
try:
victim = pieces[i]
except IndexError:
break
if i > 0 and victim.lower() == "and" and i+1 < len(pieces):
extra += 1
i += 1
victim = pieces[i]
victim = get_victim(cli, nick, victim, False)
if not victim:
return
if victim == nick:
pm(cli, nick, messages["no_suicide"])
return
if in_wolflist(nick, victim):
pm(cli, nick, messages["wolf_no_target_wolf"])
return
orig.append(victim)
target = users._get(victim) # FIXME
evt = Event("targeted_command", {"target": target, "misdirection": True, "exchange": True})
evt.dispatch(var, "kill", wolf, target, frozenset({"detrimental"}))
if evt.prevent_default:
return
victim = evt.data["target"].nick
victims.append(victim)
i += 1
if len(set(victims)) < len(victims):
pm(cli, nick, messages["wolf_must_target_multiple"])
return
KILLS[nick] = victims
if len(orig) > 1:
# need to expand this eventually (only accomodates 2 kills, whereas we should ideally support arbitrarily many)
pm(cli, nick, messages["player_kill_multiple"].format(orig[0], orig[1]))
msg = messages["wolfchat_kill_multiple"].format(nick, orig[0], orig[1])
debuglog("{0} ({1}) KILL: {2} ({3}) and {4} ({5})".format(nick, role, victims[0], get_role(victims[0]), victims[1], get_role(victims[1])))
else:
pm(cli, nick, messages["player_kill"].format(orig[0]))
msg = messages["wolfchat_kill"].format(nick, orig[0])
if num_kills > 1:
pm(cli, nick, messages["wolf_target_second"])
debuglog("{0} ({1}) KILL: {2} ({3})".format(nick, role, victims[0], get_role(victims[0])))
if in_wolflist(nick, nick):
relay_wolfchat_command(cli, nick, msg, var.WOLF_ROLES, is_wolf_command=True, is_kill_command=True)
@cmd("retract", "r", chan=False, pm=True, playing=True, phases=("night",))
def wolf_retract(cli, nick, chan, rest):
"""Removes a wolf's kill selection."""
if nick in KILLS:
del KILLS[nick]
pm(cli, nick, messages["retracted_kill"])
relay_wolfchat_command(cli, nick, messages["wolfchat_retracted_kill"].format(nick), var.WOLF_ROLES, is_wolf_command=True, is_kill_command=True)
if users._get(nick) in var.ROLES["alpha wolf"] and nick in var.BITE_PREFERENCES: # FIXME
del var.BITE_PREFERENCES[nick]
var.ALPHA_WOLVES.remove(nick)
pm(cli, nick, messages["no_bite"])
relay_wolfchat_command(cli, nick, messages["wolfchat_no_bite"].format(nick), ("alpha wolf",), is_wolf_command=True)
@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 a,b in list(KILLS.items()):
for n in b:
if n == user.nick:
KILLS[a].remove(user.nick)
if a == user.nick or len(KILLS[a]) == 0:
del KILLS[a]
@event_listener("rename_player")
def on_rename(evt, var, prefix, nick):
kvp = []
for a,b in KILLS.items():
nl = []
for n in b:
if n == prefix:
n = nick
nl.append(n)
if a == prefix:
a = nick
kvp.append((a,nl))
KILLS.update(kvp)
if prefix in KILLS:
del KILLS[prefix]
@event_listener("night_acted")
def on_acted(evt, var, user, actor):
if user.nick 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:
if p:
# kill target starting with ! is invalid
# right now nothing does this, but monster eventually will
if p[0] == "!":
continue
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:
victim = random.choice(dups)
user = users._get(victim) # FIXME
evt.data["victims"].append(user)
evt.data["bywolves"].add(user)
evt.data["onlybywolves"].add(user)
# special key to let us know to randomly select a wolf in case of retribution totem
evt.data["killers"][user].append("@wolves")
del found[victim]
# this should be moved to an event in kill, where monster prefixes their nick with !
# and fallen angel subsequently removes the ! prefix
# TODO: when monster is split off
if len(var.ROLES["fallen angel"]) == 0:
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("exchange_roles", priority=2)
def on_exchange(evt, var, actor, target, actor_role, target_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 target_role in wcroles and actor_role not in wcroles:
pl = get_players()
random.shuffle(pl)
pl.remove(actor) # remove self from list
notify = []
to_send = []
for player in pl:
prole = get_main_role(player)
if player is target:
prole = actor_role
wevt = Event("wolflist", {"tags": set()})
wevt.dispatch(var, player, actor)
tags = " ".join(wevt.data["tags"])
if prole in wcroles:
if tags:
tags += " "
to_send.append("\u0002{0}\u0002 ({1}{2})".format(player, tags, prole))
notify.append(player)
elif tags:
to_send.append("{0} ({1})".format(player, tags))
else:
to_send.append(player.nick)
for player in notify:
player.queue_message(messages["players_exchanged_roles"].format(target, actor))
if notify:
player.send_messages()
evt.data["actor_messages"].append("Players: " + ", ".join(to_send))
if target_role in CAN_KILL and var.DISEASED_WOLVES:
evt.data["actor_messages"].append(messages["ill_wolves"])
if var.ALPHA_ENABLED and target_role == "alpha wolf" and actor.nick not in var.ALPHA_WOLVES:
evt.data["actor_messages"].append(messages["wolf_bite"])
elif actor_role in wcroles and target_role not in wcroles:
pl = get_players()
random.shuffle(pl)
pl.remove(target) # remove self from list
notify = []
to_send = []
for player in pl:
prole = get_main_role(player)
if player is actor:
prole = target_role
wevt = Event("wolflist", {"tags": set()})
wevt.dispatch(var, player, target)
tags = " ".join(wevt.data["tags"])
if prole in wcroles:
if tags:
tags += " "
to_send.append("\u0002{0}\u0002 ({1}{2})".format(player, tags, prole))
notify.append(player)
elif tags:
to_send.append("{0} ({1})".format(player, tags))
else:
to_send.append(player.nick)
for player in notify:
player.queue_message(messages["players_exchanged_roles"].format(actor, target))
if notify:
player.send_messages()
evt.data["target_messages"].append("Players: " + ", ".join(to_send))
if actor_role in CAN_KILL and var.DISEASED_WOLVES:
evt.data["target_messages"].append(messages["ill_wolves"])
if var.ALPHA_ENABLED and actor_role == "alpha wolf" and target.nick not in var.ALPHA_WOLVES:
evt.data["target_messages"].append(messages["wolf_bite"])
if actor.nick in KILLS:
del KILLS[actor.nick]
if target.nick in KILLS:
del KILLS[target.nick]
@event_listener("chk_nightdone", priority=3)
def on_chk_nightdone(evt, var):
if not var.DISEASED_WOLVES:
evt.data["actedcount"] += len(KILLS)
evt.data["nightroles"].extend(get_players(CAN_KILL))
@event_listener("chk_nightdone", priority=20)
def on_chk_nightdone2(evt, var):
if not evt.prevent_default:
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 num_kills == 0 or len(wofls) == 0:
return
# flatten KILLS
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("Players: " + ", ".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("succubus_visit")
def on_succubus_visit(evt, var, succubus, target):
if get_all_players(("succubus",)).intersection(users._get(x) for x in KILLS.get(target.nick, ())): # FIXME: once KILLS holds User instances
for s in get_all_players(("succubus",)):
if s.nick in KILLS[target.nick]:
target.send(messages["no_kill_succubus"].format(succubus))
KILLS[target.nick].remove(s.nick)
if not KILLS[target.nick]:
del KILLS[target.nick]
@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: