Split Mad Scientist (#295)

* Split Mad Scientist

Related changes:
- MS now honors all protections instead of only caring about blessed
  villagers
- Allow FA to bypass protection even if FA is applied as a template
- Make bodyguard swap themselves in as the assassination target rather
  than blocking the attempt outright and suiciding. This means that
  active protections on the bodyguard have a chance to save them as well.
- Redo some messages to make the above sound nice in the bot.
- Add some additional params to the assassinate event to see WHY the
  assassination is happening (source) and WHO is doing the assassination
  (killer; currently a nick but should be a user sometime in the future).
- Add a target data item to teh assassinate event so that listeners can
  change who is being assassinated. Protection boilerplate has been
  adjusted to account for this.
- Add helper function to get the targets, avoiding code duplication
This commit is contained in:
Ryan Schmidt 2017-08-16 11:53:21 -07:00 committed by Em Barry
parent ef3c670a0d
commit 4bfa5f16f6
6 changed files with 197 additions and 124 deletions

View File

@ -198,7 +198,7 @@
"lover_suicide_no_reveal": "Saddened by the loss of their lover, \u0002{0}\u0002 commits suicide.",
"assassin_fail_totem": "Before dying, \u0002{0}\u0002 quickly attempts to slit \u0002{1}\u0002's throat; however, {1}'s totem emits a brilliant flash of light, causing the attempt to miss.",
"assassin_fail_angel": "Before dying, \u0002{0}\u0002 quickly attempts to slit \u0002{1}\u0002's throat; however, a guardian angel was on duty and able to foil the attempt.",
"assassin_fail_bodyguard": "Before dying, \u0002{0}\u0002 quickly attempts to slit \u0002{1}\u0002's throat; however, \u0002{2}\u0002, a bodyguard, sacrificed their life to protect them.",
"assassin_fail_bodyguard": "Sensing danger, \u0002{2}\u0002 shoves \u0002{1}\u0002 aside to save them from \u0002{0}\u0002.",
"assassin_success": "Before dying, \u0002{0}\u0002 quickly slits \u0002{1}\u0002's throat. The village mourns the loss of a{2} \u0002{3}\u0002.",
"assassin_success_no_reveal": "Before dying, \u0002{0}\u0002 quickly slits \u0002{1}\u0002's throat.",
"time_lord_dead": "Tick tock! Since the time lord has died, day will now only last {0} seconds and night will now only last {1} seconds!",
@ -209,6 +209,9 @@
"mad_scientist_kill_single": "\u0002{0}\u0002 throws a potent chemical concoction into the crowd. \u0002{1}\u0002, a{2} \u0002{3}\u0002, gets hit by the chemicals and dies.",
"mad_scientist_kill_single_no_reveal": "\u0002{0}\u0002 throws a potent chemical concoction into the crowd. \u0002{1}\u0002 gets hit by the chemicals and dies.",
"mad_scientist_fail": "\u0002{0}\u0002 throws a potent chemical concoction into the crowd. Thankfully, nobody seems to have gotten hit.",
"mad_scientist_fail_totem": "Sensing danger, \u0002{1}\u0002's totem emits a brilliant flash of light, teleporting them away from \u0002{0}\u0002.",
"mad_scientist_fail_angel": "Sensing danger, a guardian angel whisks \u0002{1}\u0002 away from \u0002{0}\u0002.",
"mad_scientist_fail_bodyguard": "Sensing danger, \u0002{2}\u0002 shoves \u0002{1}\u0002 aside to save them from \u0002{0}\u0002.",
"hunter_discard": "Your target has died, so you may now pick a new one.",
"wild_child_already_picked": "You have already picked your idol for this game.",
"wild_child_success": "You have picked {0} to be your idol for this game.",
@ -726,7 +729,7 @@
"succubus_win": "Game over! The {0} {1} completely enthralled the village, making them officers in an ever-growing army set on spreading their {2} control and influence throughout the entire world.",
"dullahan_die_totem": "Before dying, \u0002{0}\u0002 snaps a whip made of a human spine at \u0002{1}\u0002; however, {1}'s totem emits a brilliant flash of light, causing the attempt to miss.",
"dullahan_die_angel": "Before dying, \u0002{0}\u0002 snaps a whip made of a human spine at \u0002{1}\u0002; however, a guardian angel was on duty and able to foil the attempt.",
"dullahan_die_bodyguard": "Before dying, \u0002{0}\u0002 snaps a whip made of a human spine at \u0002{1}\u0002; however, \u0002{2}\u0002, a bodyguard, sacrificed their life to protect them.",
"dullahan_die_bodyguard": "Sensing danger, \u0002{2}\u0002 shoves \u0002{1}\u0002 aside to save them from \u0002{0}\u0002.",
"assassin_fail_blessed": "\u0002{0}\u0002 seems to be blessed, causing your assassination attempt to fail.",
"dullahan_die_success": "Before dying, \u0002{0}\u0002 snaps a whip made of a human spine at \u0002{1}\u0002, killing them. The village mourns the loss of a{2} \u0002{3}\u0002.",
"dullahan_die_success_noreveal": "Before dying, \u0002{0}\u0002 snaps a whip made of a human spine at \u0002{1}\u0002, killing them.",

View File

@ -7,7 +7,7 @@ from collections import defaultdict
import botconfig
import src.settings as var
from src.utilities import *
from src import debuglog, errlog, plog
from src import users, debuglog, errlog, plog
from src.decorators import cmd, event_listener
from src.messages import messages
from src.events import Event
@ -277,8 +277,8 @@ def on_assassinate(evt, cli, var, nick, target, prot):
for bg in var.ROLES["bodyguard"]:
if GUARDED.get(bg) == target:
cli.msg(botconfig.CHANNEL, messages[evt.params.message_prefix + "bodyguard"].format(nick, target, bg))
evt.params.del_player(cli, bg, True, end_game=False, killer_role=evt.params.nickrole, deadlist=evt.params.deadlist, original=evt.params.original, ismain=False)
evt.data["pl"] = evt.params.refresh_pl(evt.data["pl"])
# redirect the assassination to the bodyguard
evt.data["target"] = users._get(bg) # FIXME
break
@event_listener("begin_day")

View File

@ -70,25 +70,34 @@ def on_del_player(evt, cli, var, nick, mainrole, allroles, death_triggers):
pl = evt.data["pl"]
targets = TARGETS[users._get(nick)].intersection(users._get(x) for x in pl) # FIXME
if targets:
target = random.choice(list(targets)).nick
prots = deque(var.ACTIVE_PROTECTIONS[target])
aevt = Event("assassinate", {"pl": evt.data["pl"]},
target = random.choice(list(targets))
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="dullahan_die_",
source="dullahan",
killer=nick,
killer_mainrole=mainrole,
killer_allroles=allroles,
prots=prots)
while len(prots) > 0:
# an event can read the current active protection and cancel the totem
# an event can read the current active protection and cancel or redirect 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(cli, var, nick, target, prots[0]):
if not aevt.dispatch(cli, var, nick, target.nick, prots[0]):
evt.data["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
return
prots.popleft()
target = target.nick # FIXME
if var.ROLE_REVEAL in ("on", "team"):
role = get_reveal_role(target)
an = "n" if role.startswith(("a", "e", "i", "o", "u")) else ""

View File

@ -44,7 +44,7 @@ def on_transition_day(evt, cli, var):
def on_assassinate(evt, cli, var, nick, target, prot):
# bypass all protection if FA is doing the killing
# we do this by stopping propagation, meaning future events won't fire
if evt.params.nickrole == "fallen angel":
if "fallen angel" in evt.params.killer_allroles:
evt.params.prots.clear()
evt.stop_processing = True
evt.prevent_default = True

162
src/roles/madscientist.py Normal file
View File

@ -0,0 +1,162 @@
import re
import random
import itertools
import math
from collections import defaultdict, deque
import botconfig
import src.settings as var
from src.utilities import *
from src import channels, users, debuglog, errlog, plog
from src.decorators import command, event_listener
from src.messages import messages
from src.events import Event
def _get_targets(var, pl, nick):
"""Gets the mad scientist's targets.
var - settings module
pl - list of alive players
nick - nick of the mad scientist"""
for index, user in enumerate(var.ALL_PLAYERS):
if user.nick == nick: # FIXME
break
num_players = len(var.ALL_PLAYERS)
target1 = var.ALL_PLAYERS[index - 1]
target2 = var.ALL_PLAYERS[(index + 1) % num_players]
if num_players >= var.MAD_SCIENTIST_SKIPS_DEAD_PLAYERS:
# determine left player
i = index
while True:
i = (i - 1) % num_players
if var.ALL_PLAYERS[i].nick in pl or var.ALL_PLAYERS[i].nick == nick:
target1 = var.ALL_PLAYERS[i]
break
# determine right player
i = index
while True:
i = (i + 1) % num_players
if var.ALL_PLAYERS[i].nick in pl or var.ALL_PLAYERS[i].nick == nick:
target2 = var.ALL_PLAYERS[i]
break
return (target1, target2)
@event_listener("del_player")
def on_del_player(evt, cli, var, nick, mainrole, allroles, death_triggers):
if not death_triggers or "mad scientist" not in allroles:
return
pl = evt.data["pl"]
target1, target2 = _get_targets(var, pl, nick)
# apply protections (if applicable)
prots1 = deque(var.ACTIVE_PROTECTIONS[target1.nick])
prots2 = deque(var.ACTIVE_PROTECTIONS[target2.nick])
# for this event, we don't tell the event that the other side is dying
# this allows, e.g. a bodyguard and the person they are guarding to get splashed,
# and the bodyguard to still sacrifice themselves to guard the other person
aevt = Event("assassinate", {"pl": pl, "target": target1},
del_player=evt.params.del_player,
deadlist=evt.params.deadlist,
original=evt.params.original,
refresh_pl=evt.params.refresh_pl,
message_prefix="mad_scientist_fail_",
source="mad scientist",
killer=nick,
killer_mainrole=mainrole,
killer_allroles=allroles,
prots=prots1)
while len(prots1) > 0:
# events may be able to cancel this kill
if not aevt.dispatch(cli, var, nick, target1.nick, prots1[0]):
pl = aevt.data["pl"]
if target1 is not aevt.data["target"]:
target1 = aevt.data["target"]
prots1 = deque(var.ACTIVE_PROTECTIONS[target1.nick])
aevt.params.prots = prots1
continue
break
prots1.popleft()
aevt.data["target"] = target2
aevt.params.prots = prots2
while len(prots2) > 0:
# events may be able to cancel this kill
if not aevt.dispatch(cli, var, nick, target2.nick, prots2[0]):
pl = aevt.data["pl"]
if target2 is not aevt.data["target"]:
target2 = aevt.data["target"]
prots2 = deque(var.ACTIVE_PROTECTIONS[target2.nick])
aevt.params.prots = prots2
continue
break
prots2.popleft()
kill1 = target1.nick in pl and len(prots1) == 0
kill2 = target2.nick in pl and len(prots2) == 0 and target1 is not target2
if kill1:
if kill2:
if var.ROLE_REVEAL in ("on", "team"):
r1 = get_reveal_role(target1.nick)
an1 = "n" if r1.startswith(("a", "e", "i", "o", "u")) else ""
r2 = get_reveal_role(target2.nick)
an2 = "n" if r2.startswith(("a", "e", "i", "o", "u")) else ""
tmsg = messages["mad_scientist_kill"].format(nick, target1, an1, r1, target2, an2, r2)
else:
tmsg = messages["mad_scientist_kill_no_reveal"].format(nick, target1, target2)
cli.msg(botconfig.CHANNEL, tmsg)
debuglog(nick, "(mad scientist) KILL: {0} ({1}) - {2} ({3})".format(target1, get_role(target1.nick), target2, get_role(target2.nick)))
# here we DO want to tell that the other one is dying already so chained deaths don't mess things up
deadlist1 = evt.params.deadlist[:]
deadlist1.append(target2)
deadlist2 = evt.params.deadlist[:]
deadlist2.append(target1)
evt.params.del_player(cli, target1.nick, True, end_game=False, killer_role="mad scientist", deadlist=deadlist1, original=evt.params.original, ismain=False)
evt.params.del_player(cli, target2.nick, True, end_game=False, killer_role="mad scientist", deadlist=deadlist2, original=evt.params.original, ismain=False)
pl = evt.params.refresh_pl(pl)
else:
if var.ROLE_REVEAL in ("on", "team"):
r1 = get_reveal_role(target1.nick)
an1 = "n" if r1.startswith(("a", "e", "i", "o", "u")) else ""
tmsg = messages["mad_scientist_kill_single"].format(nick, target1, an1, r1)
else:
tmsg = messages["mad_scientist_kill_single_no_reveal"].format(nick, target1)
cli.msg(botconfig.CHANNEL, tmsg)
debuglog(nick, "(mad scientist) KILL: {0} ({1})".format(target1, get_role(target1.nick)))
evt.params.del_player(cli, target1.nick, True, end_game=False, killer_role="mad scientist", deadlist=evt.params.deadlist, original=evt.params.original, ismain=False)
pl = evt.params.refresh_pl(pl)
else:
if kill2:
if var.ROLE_REVEAL in ("on", "team"):
r2 = get_reveal_role(target2.nick)
an2 = "n" if r2.startswith(("a", "e", "i", "o", "u")) else ""
tmsg = messages["mad_scientist_kill_single"].format(nick, target2, an2, r2)
else:
tmsg = messages["mad_scientist_kill_single_no_reveal"].format(nick, target2)
cli.msg(botconfig.CHANNEL, tmsg)
debuglog(nick, "(mad scientist) KILL: {0} ({1})".format(target2, get_role(target2.nick)))
evt.params.del_player(cli, target2.nick, True, end_game=False, killer_role="mad scientist", deadlist=evt.params.deadlist, original=evt.params.original, ismain=False)
pl = evt.params.refresh_pl(pl)
else:
tmsg = messages["mad_scientist_fail"].format(nick)
cli.msg(botconfig.CHANNEL, tmsg)
debuglog(nick, "(mad scientist) KILL FAIL")
evt.data["pl"] = pl
@event_listener("transition_night_end", priority=2)
def on_transition_night_end(evt, cli, var):
for ms in var.ROLES["mad scientist"]:
pl = list_players()
target1, target2 = _get_targets(var, pl, ms)
if ms in var.PLAYERS and not is_user_simple(ms):
pm(cli, ms, messages["mad_scientist_notify"].format(target1, target2))
else:
pm(cli, ms, messages["mad_scientist_simple"].format(target1, target2))
# vim: set sw=4 expandtab:

View File

@ -2487,13 +2487,16 @@ def del_player(cli, nick, forced_death=False, devoice=True, end_game=True, death
target = var.TARGETED[nick]
del var.TARGETED[nick]
if target is not None and target in pl:
targuser = users._get(target) # FIXME
prots = deque(var.ACTIVE_PROTECTIONS[target])
aevt = Event("assassinate", {"pl": pl},
aevt = Event("assassinate", {"pl": pl, "target": targuser},
del_player=del_player,
deadlist=deadlist,
original=original,
refresh_pl=refresh_pl,
message_prefix="assassin_fail_",
source="assassin",
killer=nick,
killer_mainrole=nickrole,
killer_allroles=allroles,
prots=prots)
@ -2503,6 +2506,12 @@ def del_player(cli, nick, forced_death=False, devoice=True, end_game=True, death
# so that it cannot be used again (if the protection is meant to be usable once-only)
if not aevt.dispatch(cli, var, nick, target, prots[0]):
pl = aevt.data["pl"]
if targuser is not aevt.data["target"]:
targuser = aevt.data["target"]
target = targuser.nick
prots = deque(var.ACTIVE_PROTECTIONS[target])
aevt.params.prots = prots
continue
break
prots.popleft()
if len(prots) == 0:
@ -2513,8 +2522,8 @@ def del_player(cli, nick, forced_death=False, devoice=True, end_game=True, death
else:
message = messages["assassin_success_no_reveal"].format(nick, target)
cli.msg(botconfig.CHANNEL, message)
debuglog("{0} ({1}) ASSASSINATE: {2} ({3})".format(nick, nickrole, target, get_role(target)))
del_player(cli, target, True, end_game = False, killer_role = nickrole, deadlist = deadlist, original = original, ismain = False)
debuglog("{0} (assassin) ASSASSINATE: {1} ({2})".format(nick, target, get_role(target)))
del_player(cli, target, True, end_game=False, killer_role=nickrole, deadlist=deadlist, original=original, ismain=False)
pl = refresh_pl(pl)
if nickrole == "time lord":
if "DAY_TIME_LIMIT" not in var.ORIGINAL_SETTINGS:
@ -2567,86 +2576,6 @@ def del_player(cli, nick, forced_death=False, devoice=True, end_game=True, death
debuglog(nick, "(time lord) TRIGGER")
if nickrole == "mad scientist":
# kills the 2 players adjacent to them in the original players listing (in order of !joining)
# if those players are already dead, nothing happens
for index, user in enumerate(var.ALL_PLAYERS):
if user.nick == nick: # FIXME
break
targets = []
target1 = var.ALL_PLAYERS[index - 1]
target2 = var.ALL_PLAYERS[index + 1 if index < len(var.ALL_PLAYERS) - 1 else 0]
if len(var.ALL_PLAYERS) >= var.MAD_SCIENTIST_SKIPS_DEAD_PLAYERS:
# determine left player
i = index
while True:
i -= 1
if var.ALL_PLAYERS[i].nick in pl or var.ALL_PLAYERS[i].nick == nick:
target1 = var.ALL_PLAYERS[i]
break
# determine right player
i = index
while True:
i += 1
if i >= len(var.ALL_PLAYERS):
i = 0
if var.ALL_PLAYERS[i].nick in pl or var.ALL_PLAYERS[i].nick == nick:
target2 = var.ALL_PLAYERS[i]
break
# do not kill blessed players, they had a premonition to step out of the way before the chemicals hit
if target1.nick in var.ROLES["blessed villager"]:
target1 = None
if target2.nick in var.ROLES["blessed villager"]:
target2 = None
if target1.nick in pl:
if target2.nick in pl and target1 is not target2:
if var.ROLE_REVEAL in ("on", "team"):
r1 = get_reveal_role(target1.nick)
an1 = "n" if r1.startswith(("a", "e", "i", "o", "u")) else ""
r2 = get_reveal_role(target2.nick)
an2 = "n" if r2.startswith(("a", "e", "i", "o", "u")) else ""
tmsg = messages["mad_scientist_kill"].format(nick, target1, an1, r1, target2, an2, r2)
else:
tmsg = messages["mad_scientist_kill_no_reveal"].format(nick, target1, target2)
cli.msg(botconfig.CHANNEL, tmsg)
debuglog(nick, "(mad scientist) KILL: {0} ({1}) - {2} ({3})".format(target1, get_role(target1.nick), target2, get_role(target2.nick)))
deadlist1 = copy.copy(deadlist)
deadlist1.append(target2)
deadlist2 = copy.copy(deadlist)
deadlist2.append(target1)
del_player(cli, target1.nick, True, end_game = False, killer_role = "mad scientist", deadlist = deadlist1, original = original, ismain = False)
del_player(cli, target2.nick, True, end_game = False, killer_role = "mad scientist", deadlist = deadlist2, original = original, ismain = False)
pl = refresh_pl(pl)
else:
if var.ROLE_REVEAL in ("on", "team"):
r1 = get_reveal_role(target1.nick)
an1 = "n" if r1.startswith(("a", "e", "i", "o", "u")) else ""
tmsg = messages["mad_scientist_kill_single"].format(nick, target1, an1, r1)
else:
tmsg = messages["mad_scientist_kill_single_no_reveal"].format(nick, target1)
cli.msg(botconfig.CHANNEL, tmsg)
debuglog(nick, "(mad scientist) KILL: {0} ({1})".format(target1, get_role(target1.nick)))
del_player(cli, target1.nick, True, end_game = False, killer_role = "mad scientist", deadlist = deadlist, original = original, ismain = False)
pl = refresh_pl(pl)
else:
if target2.nick in pl:
if var.ROLE_REVEAL in ("on", "team"):
r2 = get_reveal_role(target2.nick)
an2 = "n" if r2.startswith(("a", "e", "i", "o", "u")) else ""
tmsg = messages["mad_scientist_kill_single"].format(nick, target2, an2, r2)
else:
tmsg = messages["mad_scientist_kill_single_no_reveal"].format(nick, target2)
cli.msg(botconfig.CHANNEL, tmsg)
debuglog(nick, "(mad scientist) KILL: {0} ({1})".format(target2, get_role(target2.nick)))
del_player(cli, target2.nick, True, end_game = False, killer_role = "mad scientist", deadlist = deadlist, original = original, ismain = False)
pl = refresh_pl(pl)
else:
tmsg = messages["mad_scientist_fail"].format(nick)
cli.msg(botconfig.CHANNEL, tmsg)
debuglog(nick, "(mad scientist) KILL FAIL")
pl = refresh_pl(pl)
# i herd u liek parameters
evt_death_triggers = death_triggers and var.PHASE in var.GAME_PHASES
@ -5331,36 +5260,6 @@ def transition_night(cli):
else:
pm(cli, drunk, messages["drunk_simple"])
for ms in var.ROLES["mad scientist"]:
pl = ps[:]
for index, user in enumerate(var.ALL_PLAYERS):
if user.nick == ms:
break
targets = []
target1 = var.ALL_PLAYERS[index - 1]
target2 = var.ALL_PLAYERS[index + 1 if index < len(var.ALL_PLAYERS) - 1 else 0]
if len(var.ALL_PLAYERS) >= var.MAD_SCIENTIST_SKIPS_DEAD_PLAYERS:
# determine left player
i = index
while True:
i -= 1
if var.ALL_PLAYERS[i].nick in pl or var.ALL_PLAYERS[i].nick == ms:
target1 = var.ALL_PLAYERS[i]
break
# determine right player
i = index
while True:
i += 1
if i >= len(var.ALL_PLAYERS):
i = 0
if var.ALL_PLAYERS[i].nick in pl or var.ALL_PLAYERS[i].nick == ms:
target2 = var.ALL_PLAYERS[i]
break
if ms in var.PLAYERS and not is_user_simple(ms):
pm(cli, ms, messages["mad_scientist_notify"].format(target1, target2))
else:
pm(cli, ms, messages["mad_scientist_simple"].format(target1, target2))
for doctor in var.ROLES["doctor"]:
if doctor in var.DOCTORS and var.DOCTORS[doctor] > 0: # has immunizations remaining
pl = ps[:]