Split and convert assassin

This commit is contained in:
Vgr E. Barry 2018-04-24 13:24:38 -04:00
parent 1a446205ce
commit ecd68d15bf
4 changed files with 175 additions and 135 deletions

163
src/roles/assassin.py Normal file
View File

@ -0,0 +1,163 @@
import re
import random
import itertools
import math
from collections import defaultdict, deque
import botconfig
from src.utilities import *
from src import channels, users, debuglog, errlog, plog
from src.functions import get_players, get_all_players, get_main_role, get_reveal_role, get_target
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
TARGETED = UserDict() # type: Dict[users.User, users.User]
@command("target", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("assassin",))
def target(var, wrapper, message):
"""Pick a player as your target, killing them if you die."""
if wrapper.source in TARGETED:
wrapper.send(messages["assassin_already_targeted"])
return
target = get_target(var, wrapper, re.split(" +", message)[0])
if not target:
return
evt = Event("targeted_command", {"target": target, "misdirection": True, "exchange": True})
if not evt.dispatch(var, "target", wrapper.source, target, frozenset({"detrimental"})):
return
target = evt.data["target"]
TARGETED[wrapper.source] = target
wrapper.send(messages["assassin_target_success"].format(target))
debuglog("{0} (assassin) TARGET: {1} ({2})".format(wrapper.source, target, get_main_role(target)))
@event_listener("chk_nightdone")
def on_chk_nightdone(evt, var):
evt.data["nightroles"].extend(get_all_players(("assassin",)))
evt.data["actedcount"] += len(TARGETED)
@event_listener("transition_day", priority=8)
def on_transition_day_resolve(evt, var):
# Select a random target for assassin that isn't already going to die if they didn't target
pl = get_players()
for ass in get_all_players(("assassin",)):
if ass not in TARGETED and ass.nick not in var.SILENCED:
ps = pl[:]
ps.remove(ass)
for victim in set(evt.data["victims"]):
if victim in ps:
ps.remove(victim)
if len(ps) > 0:
target = random.choice(ps)
TARGETED[ass] = target
ass.send(messages["assassin_random"].format(target))
@event_listener("transition_night_end")
def on_transition_night_end(evt, var):
for ass in get_all_players(("assassin",)):
if ass in TARGETED:
continue # someone already targeted
pl = get_players()
random.shuffle(pl)
pl.remove(ass)
if ass in get_all_players(("village drunk",)): # FIXME: Make into an event when village drunk is split
TARGETED[ass] = random.choice(pl)
message = messages["drunken_assassin_notification"].format(TARGETED[ass])
if not ass.prefers_simple():
message += messages["assassin_info"]
ass.send(message)
else:
if ass.prefers_simple():
ass.send(messages["assassin_simple"])
else:
ass.send(messages["assassin_notify"])
ass.send("Players: " + ", ".join(p.nick for p in pl))
@event_listener("del_player")
def on_del_player(evt, var, player, mainrole, allroles, death_triggers):
if player in TARGETED.values():
for x, y in list(TARGETED.items()):
if y is player:
del TARGETED[x]
if death_triggers and "assassin" in allroles and player in TARGETED:
target = TARGETED[player]
del TARGETED[player]
if target in evt.data["pl"]:
prots = deque(var.ACTIVE_PROTECTIONS[target.nick])
aevt = Event("assassinate", {"pl": evt.data["pl"], "target": target},
del_player=evt.params.del_player,
deadlist=evt.params.deadlist,
original=evt.params.original,
refresh_pl=evt.params.refresh_pl,
message_prefix="assassin_fail_",
source="assassin",
killer=player,
killer_mainrole=mainrole,
killer_allroles=allroles,
prots=prots)
while len(prots) > 0:
# an event can read the current active protection and cancel the assassination
# if it cancels, it is responsible for removing the protection from var.ACTIVE_PROTECTIONS
# so that it cannot be used again (if the protection is meant to be usable once-only)
if not aevt.dispatch(var, player, target, prots[0]):
pl = aevt.data["pl"]
if target is not aevt.data["target"]:
target = aevt.data["target"]
prots = deque(var.ACTIVE_PROTECTIONS[target.nick])
aevt.params.prots = prots
continue
break
prots.popleft()
if not prots:
if var.ROLE_REVEAL in ("on", "team"):
role = get_reveal_role(target)
an = "n" if role.startswith(("a", "e", "i", "o", "u")) else ""
message = messages["assassin_success"].format(player, target, an, role)
else:
message = messages["assassin_success_no_reveal"].format(player, target)
channels.Main.send(message)
debuglog("{0} (assassin) ASSASSINATE: {1} ({2})".format(player, target, get_main_role(target)))
evt.params.del_player(target, end_game=False, killer_role=mainrole, deadlist=evt.params.deadlist, original=evt.params.original, ismain=False)
evt.data["pl"] = evt.params.refresh_pl(aevt.data["pl"])
@event_listener("succubus_visit")
def on_succubus_visit(evt, var, actor, target):
if target in TARGETED and TARGETED[target] in get_all_players(("succubus",)):
msg = messages["no_target_succubus"].format(TARGETED[target])
del TARGETED[target]
if target in get_all_players(("village drunk",)):
victim = random.choice(list(get_all_players() - get_all_players(("succubus",)) - {target}))
msg += messages["drunk_target"].format(victim)
TARGETED[target] = victim
target.send(msg)
@event_listener("myrole")
def on_myrole(evt, var, user):
if user in get_all_players(("assassin",)):
msg = ""
if user in TARGETED:
msg = messages["assassin_targeting"].format(TARGETED[user])
user.send(messages["assassin_role_info"].format(msg))
@event_listener("revealroles_role")
def on_revealroles_role(evt, var, user, role):
if role == "assassin" and user in TARGETED:
evt.data["special_case"].append("targeting {0}".format(TARGETED[user]))
@event_listener("reset")
def on_reset(evt, var):
TARGETED.clear()
# vim: set sw=4 expandtab:

View File

@ -55,16 +55,7 @@ def hvisit(var, wrapper, message):
revt = Event("succubus_visit", {})
revt.dispatch(var, wrapper.source, target)
# TODO: split these into assassin, hag, and alpha wolf when they are split off
if users._get(var.TARGETED.get(target.nick), allow_none=True) in get_all_players(("succubus",)): # FIXME
msg = messages["no_target_succubus"].format(var.TARGETED[target.nick])
del var.TARGETED[target.nick]
if target in get_all_players(("village drunk",)):
victim = random.choice(list(get_all_players() - get_all_players(("succubus",)) - {target}))
msg += messages["drunk_target"].format(victim)
var.TARGETED[target.nick] = victim.nick
target.send(msg)
# TODO: split these into hag and alpha wolf when they are split off
if target.nick in var.HEXED and users._get(var.LASTHEXED[target.nick]) in get_all_players(("succubus",)): # FIXME
target.send(messages["retract_hex_succubus"].format(var.LASTHEXED[target.nick]))
var.TOBESILENCED.remove(wrapper.source.nick)

View File

@ -8,6 +8,12 @@ from src.events import Event
# handles villager and cultist
@event_listener("transition_day", priority=7)
def on_transition_day(evt, var):
for player in var.DYING:
evt.data["victims"].append(player)
evt.data["onlybywolves"].discard(player)
@event_listener("transition_night_end", priority=2)
def on_transition_night_end(evt, var):
if var.FIRST_NIGHT or var.ALWAYS_PM_ROLE:

View File

@ -2456,51 +2456,6 @@ def del_player(player, *, devoice=True, end_game=True, death_triggers=True, kill
debuglog("{0} ({1}) LOVE SUICIDE: {2} ({3})".format(lover, get_main_role(lover), player, mainrole))
del_player(lover, end_game=False, killer_role=killer_role, deadlist=deadlist, original=original, ismain=False)
pl = refresh_pl(pl)
if "assassin" in allroles:
if player.nick in var.TARGETED:
targetnick = var.TARGETED[player.nick]
del var.TARGETED[player.nick]
if targetnick is None:
target = None
else:
target = users._get(targetnick) # FIXME
if target is not None and target in pl:
prots = deque(var.ACTIVE_PROTECTIONS[target.nick])
aevt = Event("assassinate", {"pl": pl, "target": target},
del_player=del_player,
deadlist=deadlist,
original=original,
refresh_pl=refresh_pl,
message_prefix="assassin_fail_",
source="assassin",
killer=player,
killer_mainrole=mainrole,
killer_allroles=allroles,
prots=prots)
while len(prots) > 0:
# an event can read the current active protection and cancel the assassination
# if it cancels, it is responsible for removing the protection from var.ACTIVE_PROTECTIONS
# so that it cannot be used again (if the protection is meant to be usable once-only)
if not aevt.dispatch(var, player, target, prots[0]):
pl = aevt.data["pl"]
if target is not aevt.data["target"]:
target = aevt.data["target"]
prots = deque(var.ACTIVE_PROTECTIONS[target.nick])
aevt.params.prots = prots
continue
break
prots.popleft()
if len(prots) == 0:
if var.ROLE_REVEAL in ("on", "team"):
role = get_reveal_role(target)
an = "n" if role.startswith(("a", "e", "i", "o", "u")) else ""
message = messages["assassin_success"].format(player, target, an, role)
else:
message = messages["assassin_success_no_reveal"].format(player, target)
channels.Main.send(message)
debuglog("{0} (assassin) ASSASSINATE: {1} ({2})".format(player, target, get_main_role(target)))
del_player(target, end_game=False, killer_role=mainrole, deadlist=deadlist, original=original, ismain=False)
pl = refresh_pl(pl)
if mainrole == "time lord":
if "DAY_TIME_LIMIT" not in var.ORIGINAL_SETTINGS:
var.ORIGINAL_SETTINGS["DAY_TIME_LIMIT"] = var.DAY_TIME_LIMIT
@ -2636,7 +2591,7 @@ def del_player(player, *, devoice=True, end_game=True, death_triggers=True, kill
if var.PHASE in var.GAME_PHASES:
# remove the player from variables if they're in there
if ret:
for x in (var.OBSERVED, var.TARGETED, var.LASTHEXED):
for x in (var.OBSERVED, var.LASTHEXED):
for k in list(x):
if player.nick in (k, x[k]):
del x[k]
@ -2954,7 +2909,7 @@ def rename_player(var, user, prefix):
if prefix in var.PRAYED.keys():
del var.PRAYED[prefix]
for dictvar in (var.OBSERVED, var.TARGETED, var.CLONED, var.LASTHEXED, var.BITE_PREFERENCES):
for dictvar in (var.OBSERVED, var.CLONED, var.LASTHEXED, var.BITE_PREFERENCES):
kvp = []
for a,b in dictvar.items():
if a == prefix:
@ -3340,6 +3295,8 @@ def transition_day(gameid=0):
# 5 = alpha wolf bite, other custom events that trigger after all protection stuff is resolved
# 6 = rearranging victim list (ensure bodyguard/harlot messages plays),
# fixing killers dict priority again (in case step 4 or 5 added to it)
# 7 = killer-less deaths (i.e. var.DYING)
# 8 = read-only operations
# Actually killing off the victims happens in transition_day_resolve
# We set the variables here first; listeners should mutate, not replace
# We don't need to use User containers here, as these don't persist long enough
@ -3363,10 +3320,6 @@ def transition_day(gameid=0):
})
evt.dispatch(var)
for player in var.DYING:
victims.append(player)
onlybywolves.discard(player)
# remove duplicates
victims_set = set(victims)
vappend = []
@ -3474,20 +3427,6 @@ def transition_day(gameid=0):
vappend.remove(v)
victims.append(v)
# Select a random target for assassin that isn't already going to die if they didn't target
pl = get_players()
for ass in get_all_players(("assassin",)):
if ass.nick not in var.TARGETED and ass.nick not in var.SILENCED:
ps = pl[:]
ps.remove(ass)
for victim in victims:
if victim in ps:
ps.remove(victim)
if len(ps) > 0:
target = random.choice(ps)
var.TARGETED[ass.nick] = target.nick
ass.send(messages["assassin_random"].format(target))
message = [messages["sunrise"].format(min, sec)]
# This needs to go down here since having them be their night value matters above
@ -3747,14 +3686,6 @@ def chk_nightdone():
nightroles = [p for p in nightroles if p.nick not in var.SILENCED]
if var.PHASE == "night" and actedcount >= len(nightroles):
if not event.prevent_default:
# check for assassins that have not yet targeted
# must be handled separately because assassin only acts on nights when their target is dead
# and silenced assassin shouldn't add to actedcount
for ass in var.ROLES["assassin"]:
if ass.nick not in var.TARGETED.keys() | var.SILENCED: # FIXME
return
for x, t in var.TIMERS.items():
t[0].cancel()
@ -4584,31 +4515,6 @@ def choose(cli, nick, chan, rest, sendmsg=True): # XXX: transition_day also need
debuglog("{0} ({1}) MATCH: {2} ({3}) + {4} ({5})".format(nick, get_role(nick), victim, get_role(victim), victim2, get_role(victim2)))
@cmd("target", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("assassin",))
def target(cli, nick, chan, rest):
"""Pick a player as your target, killing them if you die."""
if var.TARGETED.get(nick) is not None:
pm(cli, nick, messages["assassin_already_targeted"])
return
victim = get_victim(cli, nick, re.split(" +",rest)[0], False)
if not victim:
return
if nick == victim:
pm(cli, nick, messages["no_target_self"])
return
if is_safe(nick, victim):
pm(cli, nick, messages["no_acting_on_succubus"].format("target"))
return
victim = choose_target(nick, victim)
# assassin is a template so it will never get swapped, so don't check for exchanges with it
var.TARGETED[nick] = victim
pm(cli, nick, messages["assassin_target_success"].format(victim))
debuglog("{0} (assassin) TARGET: {1} ({2})".format(nick, victim, get_role(victim)))
@cmd("hex", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("hag",))
def hex_target(cli, nick, chan, rest):
"""Hex someone, preventing them from acting the next day and night."""
@ -5022,25 +4928,6 @@ def transition_night():
else:
lycan.send(messages["lycan_notify"])
for ass in get_all_players(("assassin",)):
if ass.nick in var.TARGETED and var.TARGETED[ass.nick] is not None:
continue # someone already targeted
pl = ps[:]
random.shuffle(pl)
pl.remove(ass)
if ass in get_all_players(("village drunk",)):
var.TARGETED[ass.nick] = random.choice(pl)
message = messages["drunken_assassin_notification"].format(var.TARGETED[ass.nick])
if not ass.prefers_simple():
message += messages["assassin_info"]
ass.send(message)
else:
if ass.prefers_simple():
ass.send(messages["assassin_simple"])
else:
ass.send(messages["assassin_notify"])
ass.send("Players: " + ", ".join(p.nick for p in pl))
for turncoat in get_all_players(("turncoat",)):
# they start out as unsided, but can change n1
if turncoat.nick not in var.TURNCOATS:
@ -5334,7 +5221,6 @@ def start(cli, nick, chan, forced = False, restart = ""):
var.GUNNERS.clear()
var.OBSERVED = {}
var.CLONED = {}
var.TARGETED = {}
var.LASTHEXED = {}
var.MATCHMAKERS = set()
var.SILENCED = set()
@ -6203,10 +6089,6 @@ def myrole(var, wrapper, message): # FIXME: Need to fix !swap once this gets con
role = "sharpshooter"
wrapper.pm(messages["gunner_simple"].format(role, var.GUNNERS[wrapper.source], "" if var.GUNNERS[wrapper.source] == 1 else "s"))
# Check assassin
if wrapper.source in var.ROLES["assassin"] and wrapper.source not in var.ROLES["amnesiac"]:
wrapper.pm(messages["assassin_role_info"].format(messages["assassin_targeting"].format(var.TARGETED[wrapper.source.nick]) if wrapper.source.nick in var.TARGETED else ""))
# Remind prophet of their role, in sleepy mode only where it is hacked into a template instead of a role
if "prophet" in var.TEMPLATE_RESTRICTIONS and wrapper.source in var.ROLES["prophet"]:
wrapper.pm(messages["prophet_simple"])
@ -6684,9 +6566,7 @@ def revealroles(var, wrapper, message):
# go through each nickname, adding extra info if necessary
for user in users:
special_case = []
if role == "assassin" and user.nick in var.TARGETED:
special_case.append("targeting {0}".format(var.TARGETED[user.nick]))
elif role == "clone" and user.nick in var.CLONED:
if role == "clone" and user.nick in var.CLONED:
special_case.append("cloning {0}".format(var.CLONED[user.nick]))
# print how many bullets normal gunners have
elif (role == "gunner" or role == "sharpshooter") and user in var.GUNNERS: