Split succubus (#284)

* Split + buff succubus

When all succubi die, all entranced people now die along with them. This
should prevent an entranced person from ratting out the succubus early
on so that they go back to their team, as they lose now even if succubus
dies. One exception is if EVERY succubus idles out, then everyone that
is entranced is freed of entrancement, as it isn't their fault that they
didn't protect their friends in that case.

Dullahans now have succubi entirely removed from their list as the
likelihood they get unentranced is low, and it's easier to implement
this way.

Ensure that entranced people can vote along with ANY succubus, even if
that vote isn't the one that succeeded. Before there were cases where
they could vote along with succubus but still end up dying (particularly
in respect to a vote passing when a succubus abstained).

Clear up some message wording with regards to succubi.

Cleaned up chk_win_conditions and eliminated chk_traitor, so they make
much more sense now.

Also fixed minor issues, such as end-game saying "same number" of wolves
even if there are more wolves than villagers, hunter/vigilante dying
during night sometimes not clearing variables correctly (thus causing
premature night end) and some various stupidity going on with some old
code I wrote that doesn't have any visible effects.

* Combine all players into the same succubus death message

* Fix stylistic issues and succubus idling not working
This commit is contained in:
Ryan Schmidt 2017-01-27 12:08:41 -07:00 committed by Emanuel Barry
parent 54ab59a36f
commit 69fa7d377f
18 changed files with 555 additions and 379 deletions

View File

@ -108,7 +108,8 @@
"monster_win": "Game over! All the wolves are dead! As the villagers start preparing the BBQ, the monster{0} quickly kill{1} the remaining villagers, causing the monster{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{0}, causing the monster{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": "Game over! There are the same number of wolves as uninjured villagers. The wolves overpower the villagers and win.",
"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.",
"new_game": "\u0002{0}\u0002 has started a game of Werewolf. Type \"{1}join\" to join. Type \"{1}start\" to vote to start the game. Type \"{1}wait\" to increase the start wait time.",
"stasis": "Sorry, but {0} in stasis for {1} game{2}.",
"your_current_stasis": "You are currently in stasis for \u0002{0}\u0002 game{1}.",
@ -631,7 +632,8 @@
"vision_none": "You receive a vision that there are no \u0002{0}\u0002.",
"vision_recovering": "You are still recovering from your previous vision and are unable to receive any more visions tonight.",
"succubus_already_visited": "You are already entrancing \u0002{0}\u0002 tonight.",
"notify_succubus_target": "You have become entranced by \u0002{0}\u0002. From this point on, you must vote along with them or risk dying. For as long as they are alive, you \u0002cannot win with your own team\u0002, but you will win if \u0002{0}\u0002 wins as well.",
"succubus_not_self": "You may not entrance yourself. Use \"pass\" to not entrance anyone tonight.",
"notify_succubus_target": "You have become entranced by \u0002{0}\u0002. From this point on, you must vote along with them or risk dying. You \u0002cannot win with your own team\u0002, but you will win should all alive players become entranced.",
"succubus_harlot_success": "You have entranced \u0002{0}\u0002.",
"succubus_target_success": "You are entrancing \u0002{0}\u0002 tonight.",
"no_kill_succubus": "You discover that \u0002{0}\u0002 is a succubus and have retracted your kill as a result.",
@ -639,7 +641,7 @@
"drunk_target": " In your drunken stupor, you have selected \u0002{0}\u0002 as your target.",
"retract_totem_succubus": "You discover that \u0002{0}\u0002 is a succubus and have retracted your totem as a result.",
"retract_hex_succubus": "You discover that \u0002{0}\u0002 is a succubus and have retracted your hex as a result.",
"dullahan_no_kill_succubus": "While you remain entranced, the succubus does not need to die for you to win.",
"dullahan_no_kill_succubus": "The succubus no longer needs to die for you to win.",
"no_see_wolf": "Seeing another wolf would be a waste.",
"doomsayer_death": "You have a vision that \u0002{0}\u0002 will meet an untimely end tonight.",
"doomsayer_lycan": "You have a vision that \u0002{0}\u0002 is transforming into a savage beast tomorrow night.",
@ -723,7 +725,8 @@
"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.",
"entranced_revert_win": "With all of the succubi dead, you are no longer entranced. \u0002Your win conditions have reset to normal.\u0002",
"entranced_revert_win": "You are no longer entranced. \u0002Your win conditions have reset to normal.\u0002",
"succubus_die_kill": "As the last remaining succubus dies, a foul curse causes {0} to wither away and die in front of the astonished village.",
"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.",
"consecrating_no_vote": "You are consecrating someone today and cannot participate in the vote.",
"illness_no_vote": "You are staying home due to your illness and cannot participate in the vote.",

View File

@ -3,7 +3,7 @@ import math
import threading
import copy
from datetime import datetime
from collections import OrderedDict
from collections import defaultdict, OrderedDict
import botconfig
import src.settings as var
@ -104,7 +104,7 @@ class GameMode:
pass
# Here so any game mode can use it
def lovers_chk_win(self, evt, var, lpl, lwolves, lrealwolves):
def lovers_chk_win(self, evt, cli, var, rolemap, lpl, lwolves, lrealwolves):
winner = evt.data["winner"]
if winner is not None and winner.startswith("@"):
return # fool won, lovers can't win even if they would
@ -119,7 +119,7 @@ class GameMode:
evt.data["additional_winners"] = list(lovers)
evt.data["message"] = messages["lovers_win"]
def all_dead_chk_win(self, evt, var, lpl, lwolves, lrealwolves):
def all_dead_chk_win(self, evt, cli, var, rolemap, lpl, lwolves, lrealwolves):
if evt.data["winner"] == "no_team_wins":
evt.data["winner"] = "everyone"
evt.data["message"] = messages["everyone_died_won"]
@ -614,17 +614,12 @@ class RandomMode(GameMode):
addroles["gunner"] = random.randrange(int(len(villagers) ** 1.2 / 4))
addroles["assassin"] = random.randrange(max(int(len(villagers) ** 1.2 / 8), 1))
lpl = len(villagers)
lwolves = sum(addroles[r] for r in var.WOLFCHAT_ROLES)
lcubs = addroles["wolf cub"]
lrealwolves = sum(addroles[r] for r in var.WOLF_ROLES - {"wolf cub"})
lmonsters = addroles["monster"]
ldemoniacs = addroles["demoniac"]
ltraitors = addroles["traitor"]
lpipers = addroles["piper"]
lsuccubi = addroles["succubus"]
rolemap = defaultdict(set)
for r,c in addroles.items():
if c > 0:
rolemap[r] = set(range(c))
if chk_win_conditions(lpl, lwolves, lcubs, lrealwolves, lmonsters, ldemoniacs, ltraitors, lpipers, lsuccubi, 0, cli, end_game=False):
if chk_win_conditions(cli, rolemap, end_game=False):
return self.role_attribution(evt, cli, var, chk_win_conditions, villagers)
evt.prevent_default = True
@ -1221,38 +1216,13 @@ class MaelstromMode(GameMode):
def _on_join(self, var, wrapper):
role = random.choice(self.roles)
newlist = copy.deepcopy(var.ROLES)
newlist[role].add(wrapper.source)
lpl = len(list_players()) + 1
lwolves = len(list_players(var.WOLFCHAT_ROLES))
lcubs = len(var.ROLES["wolf cub"])
lrealwolves = len(list_players(var.WOLF_ROLES)) - lcubs
lmonsters = len(var.ROLES["monster"])
ldemoniacs = len(var.ROLES["demoniac"])
ltraitors = len(var.ROLES["traitor"])
lpipers = len(var.ROLES["piper"])
lsuccubi = len(var.ROLES["succubus"])
if role in var.WOLFCHAT_ROLES:
lwolves += 1
if role == "wolf cub":
lcubs += 1
elif role == "traitor":
ltraitors += 1
elif role in var.WOLF_ROLES:
lrealwolves += 1
elif role == "monster":
lmonsters += 1
elif role == "demoniac":
ldemoniacs += 1
elif role == "piper":
lpipers += 1
elif role == "succubus":
lsuccubi += 1
if self.chk_win_conditions(lpl, lwolves, lcubs, lrealwolves, lmonsters, ldemoniacs, ltraitors, lpipers, lsuccubi, 0, wrapper.client, end_game=False):
if self.chk_win_conditions(wrapper.client, newlist, end_game=False):
return self._on_join(var, wrapper)
var.ROLES[role].add(wrapper.source.nick)
var.ROLES[role].add(wrapper.source.nick) # FIXME: add user instead of nick
var.ORIGINAL_ROLES[role].add(wrapper.source.nick)
var.FINAL_ROLES[wrapper.source.nick] = role
var.LAST_SAID_TIME[wrapper.source.nick] = datetime.now()
@ -1353,17 +1323,12 @@ class MaelstromMode(GameMode):
if random.randrange(100) == 0 and addroles.get("villager", 0) > 0:
addroles["blessed villager"] = 1
lpl = len(villagers)
lwolves = sum(addroles[r] for r in var.WOLFCHAT_ROLES)
lcubs = addroles["wolf cub"]
lrealwolves = sum(addroles[r] for r in var.WOLF_ROLES - {"wolf cub"})
lmonsters = addroles["monster"]
ldemoniacs = addroles["demoniac"]
ltraitors = addroles["traitor"]
lpipers = addroles["piper"]
lsuccubi = addroles["succubus"]
rolemap = defaultdict(list)
for r,c in addroles.items():
if c > 0:
rolemap[r] = list(range(c))
if self.chk_win_conditions(lpl, lwolves, lcubs, lrealwolves, lmonsters, ldemoniacs, ltraitors, lpipers, lsuccubi, 0, cli, end_game=False):
if self.chk_win_conditions(cli, rolemap, end_game=False):
return self._role_attribution(cli, var, villagers, do_templates)
return addroles

View File

@ -181,7 +181,7 @@ def on_transition_day_resolve(evt, cli, var, victim):
# TODO: remove these checks once everything is split
# right now they're needed because otherwise protection may fire off even if the person isn't home
# that will not be an issue once everything is using the event
if victim in var.ROLES["harlot"] | var.ROLES["succubus"] and var.HVISITED.get(victim) and victim not in evt.data["dead"] and victim in evt.data["onlybywolves"]:
if victim in var.ROLES["harlot"] and var.HVISITED.get(victim) and victim not in evt.data["dead"] and victim in evt.data["onlybywolves"]:
return
# END checks to remove

View File

@ -40,7 +40,7 @@ def on_transition_day_resolve(evt, cli, var, victim):
# TODO: remove these checks once everything is split
# right now they're needed because otherwise protection may fire off even if the person isn't home
# that will not be an issue once everything is using the event
if victim in var.ROLES["harlot"] | var.ROLES["succubus"] and var.HVISITED.get(victim) and victim not in evt.data["dead"] and victim in evt.data["onlybywolves"]:
if victim in var.ROLES["harlot"] and var.HVISITED.get(victim) and victim not in evt.data["dead"] and victim in evt.data["onlybywolves"]:
return
# END checks to remove

View File

@ -66,7 +66,7 @@ def on_del_player(evt, cli, var, nick, nickrole, nicktpls, death_triggers):
@event_listener("get_special")
def on_get_special(evt, cli, var):
evt.data["special"].update(list_players(("detective",)))
evt.data["special"].update(var.ROLES["detective"])
@event_listener("exchange_roles")
def on_exchange(evt, cli, var, actor, nick, actor_role, nick_role):

View File

@ -101,6 +101,10 @@ def on_doctor_immunize(evt, cli, var, doctor, target):
del SICK[n]
evt.data["message"] = "not_sick"
@event_listener("get_special")
def on_get_special(evt, cli, var):
evt.data["special"].update(var.ROLES["doomsayer"])
@event_listener("chk_nightdone")
def on_chk_nightdone(evt, cli, var):
evt.data["actedcount"] += len(SEEN)

View File

@ -59,8 +59,6 @@ def on_player_win(evt, var, user, role, winner, survived):
if role != "dullahan":
return
alive = set(list_players())
if user.nick in var.ENTRANCED:
alive -= var.ROLES["succubus"]
if not TARGETS[user.nick] & alive:
evt.data["iwon"] = True
@ -138,7 +136,7 @@ def on_acted(evt, cli, var, nick, sender):
@event_listener("get_special")
def on_get_special(evt, cli, var):
evt.data["special"].update(list_players(("dullahan",)))
evt.data["special"].update(var.ROLES["dullahan"])
@event_listener("transition_day", priority=2)
def on_transition_day(evt, cli, var):
@ -205,6 +203,15 @@ def on_role_assignment(evt, cli, var, gamemode, pl, restart):
ps.remove(target)
ts.add(target)
@event_listener("succubus_visit")
def on_succubus_visit(evt, cli, var, nick, victim):
if victim in TARGETS and TARGETS[victim] & var.ROLES["succubus"]:
TARGETS.difference_update(var.ROLES["succubus"])
pm(cli, victim, messages["dullahan_no_kill_succubus"])
if KILLS.get(victim) in var.ROLES["succubus"]:
pm(cli, victim, messages["no_kill_succubus"].format(KILLS[victim]))
del KILLS[victim]
@event_listener("myrole")
def on_myrole(evt, cli, var, nick):
role = get_role(nick)

View File

@ -16,9 +16,7 @@ from src.events import Event
def on_transition_day(evt, cli, var):
# now that all protections are finished, add people back to onlybywolves
# if they're down to 1 active kill and wolves were a valid killer
# TODO: split out var.ENTRANCED_DYING when succubus is split
# that should probably be a priority 4.7 listener
victims = set(list_players()) & set(evt.data["victims"]) - var.DYING - var.ENTRANCED_DYING
victims = set(list_players()) & set(evt.data["victims"]) - var.DYING
for v in victims:
if evt.data["numkills"][v] == 1 and v in evt.data["bywolves"]:
evt.data["onlybywolves"].add(v)

View File

@ -72,14 +72,15 @@ def hunter_pass(cli, nick, chan, rest):
@event_listener("del_player")
def on_del_player(evt, cli, var, nick, nickrole, nicktpls, death_triggers):
HUNTERS.discard(nick)
PASSED.discard(nick)
if nick in KILLS:
del KILLS[nick]
for h,v in list(KILLS.items()):
if v == nick:
HUNTERS.discard(h)
PASSED.discard(h)
pm(cli, h, messages["hunter_discard"])
del KILLS[h]
elif h == nick:
del KILLS[h]
@event_listener("rename_player")
def on_rename(evt, cli, var, prefix, nick):
@ -107,7 +108,7 @@ def on_acted(evt, cli, var, nick, sender):
@event_listener("get_special")
def on_get_special(evt, cli, var):
evt.data["special"].update(list_players(("hunter",)))
evt.data["special"].update(var.ROLES["hunter"])
@event_listener("transition_day", priority=2)
def on_transition_day(evt, cli, var):
@ -149,6 +150,13 @@ def on_transition_night_end(evt, cli, var):
pm(cli, hunter, messages["hunter_simple"])
pm(cli, hunter, "Players: " + ", ".join(pl))
@event_listener("succubus_visit")
def on_succubus_visit(evt, cli, var, nick, victim):
if KILLS.get(victim) in var.ROLES["succubus"]:
pm(cli, victim, messages["no_kill_succubus"].format(KILLS[victim]))
del KILLS[victim]
HUNTERS.discard(victim)
@event_listener("begin_day")
def on_begin_day(evt, cli, var):
KILLS.clear()

View File

@ -10,9 +10,8 @@ from src.events import Event
@event_listener("exchange_roles")
def on_exchange(evt, cli, var, actor, nick, actor_role, nick_role):
special = set(list_players(("harlot", "guardian angel", "bodyguard", "priest", "prophet", "matchmaker",
"shaman", "doctor", "hag", "sorcerer", "turncoat", "clone", "crazed shaman",
"piper", "succubus")))
special = set(list_players(("harlot", "priest", "prophet", "matchmaker",
"doctor", "hag", "sorcerer", "turncoat", "clone", "piper")))
evt2 = Event("get_special", {"special": special})
evt2.dispatch(cli, var)
pl = set(list_players())
@ -39,9 +38,8 @@ def on_exchange(evt, cli, var, actor, nick, actor_role, nick_role):
@event_listener("transition_night_end", priority=2.01)
def on_transition_night_end(evt, cli, var):
# init with all roles that haven't been split yet
special = set(list_players(("harlot", "guardian angel", "bodyguard", "priest", "prophet", "matchmaker",
"shaman", "doctor", "hag", "sorcerer", "turncoat", "clone", "crazed shaman",
"piper", "succubus")))
special = set(list_players(("harlot", "priest", "prophet", "matchmaker",
"doctor", "hag", "sorcerer", "turncoat", "clone", "piper")))
evt2 = Event("get_special", {"special": special})
evt2.dispatch(cli, var)
pl = set(list_players())

View File

@ -146,7 +146,7 @@ def on_acted(evt, cli, var, nick, sender):
@event_listener("get_special")
def on_get_special(evt, cli, var):
evt.data["special"].update(list_players(("shaman",)))
evt.data["special"].update(list_players(("shaman", "crazed shaman", "wolf shaman")))
@event_listener("exchange_roles")
def on_exchange(evt, cli, var, actor, nick, actor_role, nick_role):
@ -312,12 +312,9 @@ def on_transition_day_begin(evt, cli, var):
ps = pl[:]
if LASTGIVEN.get(shaman) in ps:
ps.remove(LASTGIVEN.get(shaman))
# TODO: somehow split this off into succubus.py,
# probably via a new event
if shaman in var.ENTRANCED:
for succubus in var.ROLES["succubus"]:
if succubus in ps:
ps.remove(succubus)
levt = Event("get_random_totem_targets", {"targets": ps})
levt.dispatch(cli, var, shaman)
ps = levt.data["targets"]
if ps:
target = random.choice(ps)
totem.func(cli, shaman, shaman, target, messages["random_totem_prefix"]) # XXX: Old API
@ -440,7 +437,7 @@ def on_transition_day_resolve2(evt, cli, var, victim):
# TODO: remove these checks once everything is split
# right now they're needed because otherwise protection may fire off even if the person isn't home
# that will not be an issue once everything is using the event
if victim in var.ROLES["harlot"] | var.ROLES["succubus"] and var.HVISITED.get(victim) and victim not in evt.data["dead"] and victim in evt.data["onlybywolves"]:
if victim in var.ROLES["harlot"] and var.HVISITED.get(victim) and victim not in evt.data["dead"] and victim in evt.data["onlybywolves"]:
return
# END checks to remove
@ -455,7 +452,7 @@ def on_transition_day_resolve6(evt, cli, var, victim):
# TODO: remove these checks once everything is split
# right now they're needed because otherwise retribution may fire off when the target isn't actually dying
# that will not be an issue once everything is using the event
if victim in var.ROLES["harlot"] | var.ROLES["succubus"] and var.HVISITED.get(victim) and victim not in evt.data["dead"] and victim in evt.data["onlybywolves"]:
if victim in var.ROLES["harlot"] and var.HVISITED.get(victim) and victim not in evt.data["dead"] and victim in evt.data["onlybywolves"]:
return
if evt.data["protected"].get(victim):
return
@ -587,6 +584,13 @@ def on_assassinate(evt, cli, var, nick, target, prot):
evt.stop_processing = True
cli.msg(botconfig.CHANNEL, messages[evt.params.message_prefix + "totem"].format(nick, target))
@event_listener("succubus_visit")
def on_succubus_visit(evt, cli, var, nick, victim):
if (SHAMANS.get(victim, (None, None))[1] in var.ROLES["succubus"] and
(get_role(victim) == "crazed shaman" or TOTEMS[victim] not in var.BENEFICIAL_TOTEMS)):
pm(cli, victim, messages["retract_totem_succubus"].format(SHAMANS[victim]))
del SHAMANS[victim]
@event_listener("myrole")
def on_myrole(evt, cli, var, nick):
role = evt.data["role"]

317
src/roles/succubus.py Normal file
View File

@ -0,0 +1,317 @@
import re
import random
import itertools
import math
from collections import defaultdict
import botconfig
import src.settings as var
from src.utilities import *
from src import channels, users, debuglog, errlog, plog
from src.decorators import cmd, event_listener
from src.messages import messages
from src.events import Event
ENTRANCED = set()
ENTRANCED_DYING = set()
VISITED = {}
ALL_SUCC_IDLE = True
@cmd("visit", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("succubus",))
def hvisit(cli, nick, chan, rest):
"""Entrance a player, converting them to your team."""
if VISITED.get(nick):
pm(cli, nick, messages["succubus_already_visited"].format(VISITED[nick]))
return
victim = get_victim(cli, nick, re.split(" +",rest)[0], False, True)
if not victim:
return
if nick == victim:
pm(cli, nick, messages["succubus_not_self"])
return
evt = Event("targeted_command", {"target": victim, "misdirection": True, "exchange": False})
evt.dispatch(cli, var, "visit", nick, victim, frozenset({"detrimental", "immediate"}))
if evt.prevent_default:
return
victim = evt.data["target"]
vrole = get_role(victim)
VISITED[nick] = victim
if vrole != "succubus":
ENTRANCED.add(victim)
pm(cli, nick, messages["succubus_target_success"].format(victim))
else:
pm(cli, nick, messages["harlot_success"].format(victim))
if nick != victim:
if vrole != "succubus":
pm(cli, victim, messages["notify_succubus_target"].format(nick))
else:
pm(cli, victim, messages["harlot_success"].format(nick))
revt = Event("succubus_visit", {})
revt.dispatch(cli, var, nick, victim)
# TODO: split these into assassin, hag, and alpha wolf when they are split off
if var.TARGETED.get(victim) in var.ROLES["succubus"]:
msg = messages["no_target_succubus"].format(var.TARGETED[victim])
del var.TARGETED[victim]
if victim in var.ROLES["village drunk"]:
target = random.choice(list(set(list_players()) - var.ROLES["succubus"] - {victim}))
msg += messages["drunk_target"].format(target)
var.TARGETED[victim] = target
pm(cli, victim, nick)
if victim in var.HEXED and var.LASTHEXED[victim] in var.ROLES["succubus"]:
pm(cli, victim, messages["retract_hex_succubus"].format(var.LASTHEXED[victim]))
var.TOBESILENCED.remove(nick)
var.HEXED.remove(victim)
del var.LASTHEXED[victim]
if var.BITE_PREFERENCES.get(victim) in var.ROLES["succubus"]:
pm(cli, victim, messages["no_kill_succubus"].format(var.BITE_PREFERENCES[victim]))
del var.BITE_PREFERENCES[victim]
debuglog("{0} ({1}) VISIT: {2} ({3})".format(nick, get_role(nick), victim, vrole))
chk_nightdone(cli)
@cmd("pass", chan=False, pm=True, playing=True, phases=("night",), roles=("succubus",))
def pass_cmd(cli, nick, chan, rest):
"""Do not entrance someone tonight."""
if VISITED.get(nick):
pm(cli, nick, messages["succubus_already_visited"].format(VISITED[nick]))
return
VISITED[nick] = None
pm(cli, nick, messages["succubus_pass"])
debuglog("{0} ({1}) PASS".format(nick, get_role(nick)))
chk_nightdone(cli)
@event_listener("get_random_totem_targets")
def on_get_random_totem_targets(evt, cli, var, shaman):
if shaman in ENTRANCED:
for succubus in var.ROLES["succubus"]:
if succubus in evt.data["targets"]:
evt.data["targets"].remove(succubus)
@event_listener("chk_decision", priority=0)
def on_chk_decision(evt, cli, var, force):
for votee, voters in evt.data["votelist"].items():
if votee in var.ROLES["succubus"]:
for vtr in ENTRANCED:
if vtr in voters:
voters.remove(vtr)
def _kill_entranced_voters(var, votelist, not_lynching, votee):
if not var.ROLES["succubus"] & (set(itertools.chain(*votelist.values())) | not_lynching):
# none of the succubi voted (or there aren't any succubi), so short-circuit
return
# kill off everyone entranced that did not follow one of the succubi's votes or abstain
# unless a succubus successfully voted the target, then people that didn't follow are spared
ENTRANCED_DYING.update(ENTRANCED - var.DEAD)
for other_votee, other_voters in votelist.items():
if var.ROLES["succubus"] & set(other_voters):
if votee == other_votee:
ENTRANCED_DYING.clear()
return
ENTRANCED_DYING.difference_update(other_voters)
if var.ROLES["succubus"] & not_lynching:
if votee is None:
ENTRANCED_DYING.clear()
return
ENTRANCED_DYING.difference_update(not_lynching)
@event_listener("chk_decision_lynch", priority=5)
def on_chk_decision_lynch(evt, cli, var, voters):
# a different event may override the original votee, but people voting along with succubus
# won't necessarily know that, so base whether or not they risk death on the person originally voted
_kill_entranced_voters(var, evt.params.votelist, evt.params.not_lynching, evt.params.original_votee)
@event_listener("chk_decision_abstain")
def on_chk_decision_abstain(evt, cli, var, not_lynching):
_kill_entranced_voters(var, evt.params.votelist, not_lynching, None)
# entranced logic should run after team wins have already been determined (aka run last)
# we do not want to override the win conditions for neutral roles should they win while entranced
# For example, entranced monsters should win with other monsters should mosnters win, and be
# properly credited with a team win in that event.
@event_listener("player_win", priority=6)
def on_player_win(evt, var, user, role, winner, survived):
nick = user.nick
if nick in ENTRANCED:
evt.data["special"].append("entranced")
if winner != "succubi" and role not in var.TRUE_NEUTRAL_ROLES:
evt.data["won"] = False
else:
evt.data["iwon"] = True
if role == "succubus" and winner == "succubi":
evt.data["won"] = True
@event_listener("chk_win", priority=2)
def on_chk_win(evt, cli, var, rolemap, lpl, lwolves, lrealwolves):
lsuccubi = len(rolemap.get("succubus", ()))
lentranced = len(ENTRANCED - var.DEAD)
if var.PHASE == "day" and lpl - lsuccubi == lentranced:
evt.data["winner"] = "succubi"
evt.data["message"] = messages["succubus_win"].format(plural("succubus", lsuccubi), plural("has", lsuccubi), plural("master's", lsuccubi))
@event_listener("del_player")
def on_del_player(evt, cli, var, nick, nickrole, nicktpls, death_triggers):
global ALL_SUCC_IDLE
if nickrole != "succubus":
return
if nick in VISITED:
# if it's night, also unentrance the person they visited
if var.PHASE == "night" and var.GAMEPHASE == "night":
if VISITED[nick] in ENTRANCED:
ENTRANCED.discard(visited[nick])
ENTRANCED_DYING.discard(visited[nick])
pm(cli, VISITED[nick], messages["entranced_revert_win"])
del VISITED[nick]
# if all succubi are dead, one of two things happen:
# 1. if all succubi idled out (every last one of them), un-entrance people
# 2. otherwise, kill all entranced people immediately, they still remain entranced (and therefore lose)
# death_triggers is False for an idle-out, so we use that to determine which it is
if death_triggers:
ALL_SUCC_IDLE = False
if len(var.ROLES["succubus"]) == 0:
if ALL_SUCC_IDLE:
while ENTRANCED:
e = ENTRANCED.pop()
pm(cli, e, messages["entranced_revert_win"])
elif ENTRANCED:
msg = []
# Run in two loops so we can play the message for everyone dying at once before we actually
# kill any of them off (if we killed off first, the message order would be wrong wrt death chains)
comma = ""
if var.ROLE_REVEAL in ("on", "team"):
comma = ","
for e in ENTRANCED:
if var.ROLE_REVEAL in ("on", "team"):
role = get_reveal_role(e)
an = "n" if role.startswith(("a", "e", "i", "o", "u")) else ""
msg.append("\u0002{0}\u0002, a{1} \u0002{2}\u0002".format(e, an, role))
else:
msg.append("\u0002{0}\u0002".format(e))
if len(msg) == 1:
cli.msg(botconfig.CHANNEL, messages["succubus_die_kill"].format(msg[0] + comma))
elif len(msg) == 2:
cli.msg(botconfig.CHANNEL, messages["succubus_die_kill"].format(msg[0] + comma + " and " + msg[1] + comma))
else:
cli.msg(botconfig.CHANNEL, messages["succubus_die_kill"].format(", ".join(msg[:-1]) + ", and " + msg[-1] + comma))
for e in ENTRANCED:
# to ensure we do not double-kill someone, notify all child deaths that we'll be
# killing off everyone else that is entranced so they don't need to bother
dlc = list(evt.params.deadlist)
dlc.extend(ENTRANCED - {e})
debuglog("{0} ({1}) SUCCUBUS DEATH KILL: {2} ({3})".format(nick, nickrole, e, get_role(e)))
evt.params.del_player(cli, e, end_game=False, killer_role="succubus",
deadlist=dlc, original=evt.params.original, ismain=False)
evt.data["pl"] = evt.params.refresh_pl(evt.data["pl"])
ENTRANCED_DYING.clear()
@event_listener("transition_day_resolve", priority=1)
def on_transition_day_resolve(evt, cli, var, victim):
if victim in var.ROLES["succubus"] and VISITED.get(victim) and victim not in evt.data["dead"] and victim in evt.data["onlybywolves"]:
# TODO: check if this is necessary for succubus, it's to prevent a message playing if alpha bites
# a harlot that is visiting a wolf, since the bite succeeds in that case.
if victim not in evt.data["bitten"]:
evt.data["message"].append(messages["target_not_home"])
evt.data["novictmsg"] = False
evt.stop_processing = True
evt.prevent_default = True
@event_listener("transition_day_resolve_end", priority=1)
def on_transition_day_resolve_end(evt, cli, var, victims):
for victim in victims + evt.data["bitten"]:
if victim in evt.data["dead"] and victim in VISITED.values() and (victim in evt.data["bywolves"] or victim in evt.data["bitten"]):
for succ in VISITED:
if VISITED[succ] == victim and succ not in evt.data["bitten"] and succ not in evt.data["dead"]:
if var.ROLE_REVEAL in ("on", "team"):
evt.data["message"].append(messages["visited_victim"].format(succ, get_role(succ)))
else:
evt.data["message"].append(messages["visited_victim_noreveal"].format(succ))
evt.data["bywolves"].add(succ)
evt.data["onlybywolves"].add(succ)
evt.data["dead"].append(succ)
@event_listener("night_acted")
def on_night_acted(evt, cli, var, nick, sender):
if VISITED.get(nick):
evt.data["acted"] = True
@event_listener("chk_nightdone")
def on_chk_nightdone(evt, cli, var):
evt.data["actedcount"] += len(VISITED)
evt.data["nightroles"].extend(var.ROLES["succubus"])
@event_listener("targeted_command")
def on_targeted_command(evt, cli, var, cmd, actor, orig_target, tags):
if "beneficial" not in tags and actor in ENTRANCED and evt.data["target"] in var.ROLES["succubus"]:
try:
what = evt.params.action
except AttributeError:
what = cmd
pm(cli, actor, messages["no_acting_on_succubus"].format(what))
evt.stop_processing = True
evt.prevent_default = True
@event_listener("transition_night_end", priority=2)
def on_transition_night_end(evt, cli, var):
for succubus in var.ROLES["succubus"]:
pl = list_players()
random.shuffle(pl)
pl.remove(succubus)
if succubus in var.PLAYERS and not is_user_simple(succubus):
pm(cli, succubus, messages["succubus_notify"])
else:
pm(cli, succubus, messages["succubus_simple"])
pm(cli, succubus, "Players: " + ", ".join(("{0} ({1})".format(x, get_role(x)) if x in var.ROLES["succubus"] else x for x in pl)))
@event_listener("begin_day")
def on_begin_day(evt, cli, var):
VISITED.clear()
ENTRANCED_DYING.clear()
@event_listener("transition_day", priority=2)
def on_transition_day(evt, cli, var):
for v in ENTRANCED_DYING:
var.DYING.add(v) # indicate that the death bypasses protections
evt.data["victims"].append(v)
evt.data["onlybywolves"].discard(v)
# we do not add to killers as retribution totem should not work on entranced not following succubus
@event_listener("get_special")
def on_get_special(evt, cli, var):
evt.data["special"].update(var.ROLES["succubus"])
@event_listener("rename_player")
def on_rename(evt, cli, var, prefix, nick):
if prefix in ENTRANCED:
ENTRANCED.remove(prefix)
ENTRANCED.add(nick)
if prefix in ENTRANCED_DYING:
ENTRANCED_DYING.remove(prefix)
ENTRANCED_DYING.add(nick)
kvp = {}
for a,b in VISITED.items():
s = nick if a == prefix else a
t = nick if b == prefix else b
kvp[s] = t
VISITED.update(kvp)
if prefix in VISITED:
del VISITED[prefix]
@event_listener("reset")
def on_reset(evt, var):
global ALL_SUCC_IDLE
ALL_SUCC_IDLE = True
ENTRANCED.clear()
ENTRANCED_DYING.clear()
VISITED.clear()
@event_listener("revealroles")
def on_revealroles(evt, var, wrapper):
if ENTRANCED:
evt.data["output"].append("\u0002entranced players\u0002: {0}".format(", ".join(ENTRANCED)))
if ENTRANCED_DYING:
evt.data["output"].append("\u0002dying entranced players\u0002: {0}".format(", ".join(ENTRANCED_DYING)))
# vim: set sw=4 expandtab:

View File

@ -59,5 +59,24 @@ def on_update_stats3(evt, cli, var, nick, nickrole, nickreveal, nicktpls):
# and a wolf kill, and a wolf + villager died, we know the villager was the wolf kill
# and therefore cannot be traitor. However, we currently do not have the logic to deduce this
@event_listener("chk_win", priority=1.1)
def on_chk_win(evt, cli, var, rolemap, lpl, lwolves, lrealwolves):
did_something = False
if lrealwolves == 0:
for traitor in list(rolemap["traitor"]):
rolemap["wolf"].add(traitor)
rolemap["traitor"].remove(traitor)
rolemap["cursed villager"].discard(traitor)
did_something = True
if var.PHASE in var.GAME_PHASES:
var.FINAL_ROLES[traitor] = "wolf"
pm(cli, traitor, messages["traitor_turn"])
debuglog(traitor, "(traitor) TURNING")
if did_something:
if var.PHASE in var.GAME_PHASES:
var.TRAITOR_TURNED = True
cli.msg(botconfig.CHANNEL, messages["traitor_turn_channel"])
evt.prevent_default = True
evt.stop_processing = True
# vim: set sw=4 expandtab:

View File

@ -63,13 +63,13 @@ def vigilante_pass(cli, nick, chan, rest):
@event_listener("del_player")
def on_del_player(evt, cli, var, nick, nickrole, nicktpls, death_triggers):
PASSED.discard(nick)
if nick in KILLS:
del KILLS[nick]
for h,v in list(KILLS.items()):
if v == nick:
PASSED.discard(h)
pm(cli, h, messages["hunter_discard"])
del KILLS[h]
elif h == nick:
del KILLS[h]
@event_listener("rename_player")
def on_rename(evt, cli, var, prefix, nick):
@ -94,7 +94,7 @@ def on_acted(evt, cli, var, nick, sender):
@event_listener("get_special")
def on_get_special(evt, cli, var):
evt.data["special"].update(list_players(("vigilante",)))
evt.data["special"].update(var.ROLES["vigilante"])
@event_listener("transition_day", priority=2)
def on_transition_day(evt, cli, var):
@ -135,6 +135,12 @@ def on_transition_night_end(evt, cli, var):
pm(cli, vigilante, messages["vigilante_simple"])
pm(cli, vigilante, "Players: " + ", ".join(pl))
@event_listener("succubus_visit")
def on_succubus_visit(evt, cli, var, nick, victim):
if KILLS.get(victim) in var.ROLES["succubus"]:
pm(cli, victim, messages["no_kill_succubus"].format(KILLS[victim]))
del KILLS[victim]
@event_listener("begin_day")
def on_begin_day(evt, cli, var):
KILLS.clear()

View File

@ -49,4 +49,18 @@ def on_player_win(evt, var, user, role, winner, survived):
evt.data["won"] = True
evt.data["iwon"] = survived
@event_listener("chk_win", priority=3)
def on_chk_win(evt, cli, var, rolemap, lpl, lwolves, lrealwolves):
if evt.data["winner"] is not None:
return
if lrealwolves == 0:
evt.data["winner"] = "villagers"
evt.data["message"] = messages["villager_win"]
elif lwolves == lpl / 2:
evt.data["winner"] = "wolves"
evt.data["message"] = messages["wolf_win_equal"]
elif lwolves > lpl / 2:
evt.data["winner"] = "wolves"
evt.data["message"] = messages["wolf_win_greater"]
# vim: set sw=4 expandtab:

View File

@ -394,6 +394,33 @@ def on_transition_night_end(evt, cli, var):
if var.ALPHA_ENABLED and role == "alpha wolf" and wolf not in var.ALPHA_WOLVES:
pm(cli, wolf, messages["wolf_bite"])
@event_listener("chk_win", priority=1)
def on_chk_win(evt, cli, var, rolemap, lpl, lwolves, lrealwolves):
# TODO: split into cub
did_something = False
if lrealwolves == 0:
for wc in list(rolemap["wolf cub"]):
rolemap["wolf"].add(wc)
rolemap["wolf cub"].remove(wc)
did_something = True
if var.PHASE in var.GAME_PHASES:
var.FINAL_ROLES[wc] = "wolf"
pm(cli, wc, messages["cub_grow_up"])
debuglog(wc, "(wolf cub) GROW UP")
if did_something:
evt.prevent_default = True
evt.stop_processing = True
@event_listener("succubus_visit")
def on_succubus_visit(evt, cli, var, nick, victim):
if var.ROLES["succubus"].intersection(KILLS.get(victim, ())):
for s in var.ROLES["succubus"]:
if s in KILLS[victim]:
pm(cli, victim, messages["no_kill_succubus"].format(nick))
KILLS[victim].remove(s)
if not KILLS[victim]:
del KILLS[victim]
@event_listener("begin_day")
def on_begin_day(evt, cli, var):
KILLS.clear()

View File

@ -309,15 +309,20 @@ def singular(plural):
# otherwise we just added an s on the end
return plural[:-1]
def list_players(roles=None):
def list_players(roles=None, *, rolemap=None):
if rolemap is None:
rolemap = var.ROLES
if roles is None:
roles = var.ROLES.keys()
roles = rolemap.keys()
pl = set()
for x in roles:
if x in var.TEMPLATE_RESTRICTIONS.keys():
if x in var.TEMPLATE_RESTRICTIONS:
continue
for p in var.ROLES.get(x, ()):
pl.add(p)
pl.update(rolemap.get(x, ()))
if rolemap is not var.ROLES:
# we weren't given an actual player list (possibly),
# so the elements of pl are not necessarily in var.ALL_PLAYERS
return list(pl)
return [p.nick for p in var.ALL_PLAYERS if p.nick in pl]
def list_players_and_roles():
@ -352,10 +357,12 @@ def get_role(p):
raise ValueError("Nick {0} isn't playing and has no defined participant role".format(p))
return role
def get_roles(*roles):
def get_roles(*roles, rolemap=None):
if rolemap is None:
rolemap = var.ROLES
all_roles = []
for role in roles:
all_roles.append(var.ROLES[role])
all_roles.append(rolemap[role])
return list(itertools.chain(*all_roles))
def get_reveal_role(nick):

View File

@ -312,7 +312,6 @@ def reset():
var.GAMEMODE_VOTES = {} #list of players who have used !game
var.START_VOTES.clear() # list of players who have voted to !start
var.LOVERS = {} # need to be here for purposes of random
var.ENTRANCED = set()
var.ROLE_STATS = frozenset() # type: FrozenSet[FrozenSet[Tuple[str, int]]]
var.ROLE_SETS = [] # type: List[Tuple[Counter[str], int]]
@ -1800,33 +1799,25 @@ def chk_decision(cli, force=""):
# we only need 50%+ to not lynch, instead of an actual majority, because a tie would time out day anyway
# don't check for ABSTAIN_ENABLED here since we may have a case where the majority of people have pacifism totems or something
if len(not_lynching) >= math.ceil(avail / 2):
abs_evt = Event("chk_decision_abstain", {})
abs_evt = Event("chk_decision_abstain", {}, votelist=votelist, numvotes=numvotes)
abs_evt.dispatch(cli, var, not_lynching)
cli.msg(botconfig.CHANNEL, messages["village_abstain"])
if var.ROLES["succubus"] & var.NO_LYNCH:
var.ENTRANCED_DYING.update(var.ENTRANCED - var.NO_LYNCH - var.DEAD)
else:
var.ENTRANCED_DYING.update(var.ENTRANCED & var.NO_LYNCH)
var.ABSTAINED = True
event.data["transition_night"](cli)
return
for votee, voters in votelist.items():
# split this into a priority 0 event when it comes time to split off succubus
if votee in var.ROLES["succubus"]:
for vtr in var.ENTRANCED:
if vtr in voters:
voters.remove(vtr)
if numvotes[votee] >= votesneeded or votee == force:
# priorities:
# 1 = displaying impatience totem messages
# 3 = mayor/revealing totem
# 4 = fool
# 5 = desperation totem, other things that happen on generic lynch
vote_evt = Event("chk_decision_lynch", {
"votee": votee,
"deadlist": deadlist
}, del_player=del_player)
vote_evt = Event("chk_decision_lynch", {"votee": votee, "deadlist": deadlist},
del_player=del_player,
original_votee=votee,
force=(votee == force),
votelist=votelist,
not_lynching=not_lynching)
if vote_evt.dispatch(cli, var, voters):
votee = vote_evt.data["votee"]
# roles that end the game upon being lynched
@ -1844,23 +1835,6 @@ def chk_decision(cli, force=""):
if votee in var.ROLES["jester"]:
var.JESTERS.add(votee)
# this checks if any succubus have voted the current votee
# if it is NOT the case, then it checks if any succubus voted at all
# it does so by joining together all the values from var.VOTES and casting them all into a single set
# if any succubus voted for anyone else and nobody for the current votee, then proceed to block
if not var.ROLES["succubus"] & set(var.VOTES[votee]) and var.ROLES["succubus"] & set(itertools.chain(*var.VOTES.values())):
voted_along = set()
for person, all_voters in var.VOTES.items():
if var.ROLES["succubus"] & set(all_voters):
for v in all_voters:
if v in var.ENTRANCED:
voted_along.add(v)
var.ENTRANCED_DYING.update(var.ENTRANCED - voted_along - var.DEAD) # can be the empty set
elif var.ROLES["succubus"] & var.NO_LYNCH: # any succubus abstained
var.ENTRANCED_DYING.update(var.ENTRANCED - var.NO_LYNCH - var.DEAD)
if var.ROLE_REVEAL in ("on", "team"):
rrole = get_reveal_role(votee)
an = "n" if rrole.startswith(("a", "e", "i", "o", "u")) else ""
@ -1954,38 +1928,6 @@ def show_votes(cli, nick, chan, rest):
reply(cli, nick, chan, the_message)
# TODO: need to generalize this logic (as well as the logic in chk_win_conditions)
# once refactored, it should be split off into individual role files
def chk_traitor(cli):
realwolves = var.WOLF_ROLES - {"wolf cub"}
if len(list_players(realwolves)) > 0:
return # actual wolves still alive
wcl = copy.copy(var.ROLES["wolf cub"])
ttl = copy.copy(var.ROLES["traitor"])
event = Event("chk_traitor", {})
if event.dispatch(cli, var, wcl, ttl):
for wc in wcl:
var.ROLES["wolf"].add(wc)
var.ROLES["wolf cub"].remove(wc)
var.FINAL_ROLES[wc] = "wolf"
pm(cli, wc, messages["cub_grow_up"])
debuglog(wc, "(wolf cub) GROW UP")
if len(var.ROLES["wolf"]) == 0:
for tt in ttl:
var.ROLES["wolf"].add(tt)
var.ROLES["traitor"].remove(tt)
var.FINAL_ROLES[tt] = "wolf"
var.ROLES["cursed villager"].discard(tt)
pm(cli, tt, messages["traitor_turn"])
debuglog(tt, "(traitor) TURNING")
if len(var.ROLES["wolf"]) > 0:
var.TRAITOR_TURNED = True
cli.msg(botconfig.CHANNEL, messages["traitor_turn_channel"])
def stop_game(cli, winner="", abort=False, additional_winners=None, log=True):
chan = botconfig.CHANNEL
if abort:
@ -2131,8 +2073,6 @@ def stop_game(cli, winner="", abort=False, additional_winners=None, log=True):
pentry["templates"] = pltp[plr]
if splr in var.LOVERS:
pentry["special"].append("lover")
if splr in var.ENTRANCED:
pentry["special"].append("entranced")
won = False
iwon = False
@ -2152,16 +2092,13 @@ def stop_game(cli, winner="", abort=False, additional_winners=None, log=True):
# determine if this player's team won
if rol in var.TRUE_NEUTRAL_ROLES:
# most true neutral roles never have a team win, only individual wins. Exceptions to that are here
teams = {"monster":"monsters", "piper":"pipers", "succubus":"succubi", "demoniac":"demoniacs"}
teams = {"monster":"monsters", "piper":"pipers", "demoniac":"demoniacs"}
if rol in teams and winner == teams[rol]:
won = True
elif rol == "turncoat" and splr in var.TURNCOATS and var.TURNCOATS[splr][0] != "none":
won = (winner == var.TURNCOATS[splr][0])
elif rol == "fool" and "@" + splr == winner:
won = True
elif winner != "succubi" and splr in var.ENTRANCED:
# entranced players can't win with villager or wolf teams
won = False
if pentry["dced"]:
# You get NOTHING! You LOSE! Good DAY, sir!
@ -2179,7 +2116,7 @@ def stop_game(cli, winner="", abort=False, additional_winners=None, log=True):
if lvr in plrl:
lvrrol = plrl[lvr]
if not winner.startswith("@") and winner not in ("monsters", "demoniacs", "pipers"):
if not winner.startswith("@") and singular(winner) not in var.WIN_STEALER_ROLES:
iwon = True
break
elif winner.startswith("@") and winner == "@" + lvr and var.LOVER_WINS_WITH_FOOL:
@ -2202,12 +2139,10 @@ def stop_game(cli, winner="", abort=False, additional_winners=None, log=True):
iwon = True
elif rol == "clone":
# this means they ended game while being clone and not some other role
if splr in survived and not winner.startswith("@") and winner not in ("monsters", "demoniacs", "pipers"):
if splr in survived and not winner.startswith("@") and singular(winner) not in var.WIN_STEALER_ROLES:
iwon = True
elif rol == "jester" and splr in var.JESTERS:
iwon = True
elif winner == "succubi" and splr in var.ENTRANCED | var.ROLES["succubus"]:
iwon = True
elif not iwon:
iwon = won and splr in survived # survived, team won = individual win
@ -2301,12 +2236,25 @@ def chk_win(cli, end_game=True, winner=None):
var.ADMIN_TO_PING = None
return True
return False
if var.PHASE not in var.GAME_PHASES:
return False #some other thread already ended game probably
return chk_win_conditions(cli, var.ROLES, end_game, winner)
def chk_win_conditions(cli, rolemap, end_game=True, winner=None):
"""Internal handler for the chk_win function."""
chan = botconfig.CHANNEL
with var.GRAVEYARD_LOCK:
if var.PHASE not in ("day", "night"):
return False #some other thread already ended game probably
if var.PHASE == "day":
pl = set(list_players()) - (var.WOUNDED | var.CONSECRATING)
evt = Event("get_voters", {"voters": pl})
evt.dispatch(cli, var)
pl = evt.data["voters"]
lpl = len(pl)
else:
pl = set(list_players(rolemap=rolemap))
lpl = len(pl)
if var.RESTRICT_WOLFCHAT & var.RW_REM_NON_WOLVES:
if var.RESTRICT_WOLFCHAT & var.RW_TRAITOR_NON_WOLF:
@ -2315,32 +2263,16 @@ def chk_win(cli, end_game=True, winner=None):
wcroles = var.WOLF_ROLES | {"traitor"}
else:
wcroles = var.WOLFCHAT_ROLES
wolves = set(list_players(wcroles))
lwolves = len(wolves)
lcubs = len(var.ROLES.get("wolf cub", ()))
lrealwolves = len(list_players(var.WOLF_ROLES - {"wolf cub"}))
lmonsters = len(var.ROLES.get("monster", ()))
ldemoniacs = len(var.ROLES.get("demoniac", ()))
ltraitors = len(var.ROLES.get("traitor", ()))
lpipers = len(var.ROLES.get("piper", ()))
lsuccubi = len(var.ROLES.get("succubus", ()))
lentranced = len(var.ENTRANCED - var.DEAD)
if var.PHASE == "day":
pl = set(list_players()) - (var.WOUNDED | var.CONSECRATING)
evt = Event("get_voters", {"voters": pl})
evt.dispatch(cli, var)
pl = evt.data["voters"]
wolves = set(list_players(wcroles, rolemap=rolemap))
lwolves = len(wolves & pl)
lcubs = len(rolemap.get("wolf cub", ()))
lrealwolves = len(list_players(var.WOLF_ROLES - {"wolf cub"}, rolemap=rolemap))
lmonsters = len(rolemap.get("monster", ()))
ldemoniacs = len(rolemap.get("demoniac", ()))
ltraitors = len(rolemap.get("traitor", ()))
lpipers = len(rolemap.get("piper", ()))
lpl = len(pl)
lwolves = len(wolves & pl)
return chk_win_conditions(lpl, lwolves, lcubs, lrealwolves, lmonsters, ldemoniacs, ltraitors, lpipers, lsuccubi, lentranced, cli, end_game, winner)
def chk_win_conditions(lpl, lwolves, lcubs, lrealwolves, lmonsters, ldemoniacs, ltraitors, lpipers, lsuccubi, lentranced, cli, end_game=True, winner=None):
"""Internal handler for the chk_win function."""
chan = botconfig.CHANNEL
with var.GRAVEYARD_LOCK:
message = ""
# fool won, chk_win was called from !lynch
if winner and winner.startswith("@"):
@ -2349,9 +2281,6 @@ def chk_win_conditions(lpl, lwolves, lcubs, lrealwolves, lmonsters, ldemoniacs,
message = messages["no_win"]
# still want people like jesters, dullahans, etc. to get wins if they fulfilled their win conds
winner = "no_team_wins"
elif var.PHASE == "day" and lpl - lsuccubi == lentranced:
winner = "succubi"
message = messages["succubus_win"].format(plural("succubus", lsuccubi), plural("has", lsuccubi), plural("master's", lsuccubi))
elif var.PHASE == "day" and lpipers and len(list_players()) - lpipers == len(var.CHARMED - var.ROLES["piper"]):
winner = "pipers"
message = messages["piper_win"].format("s" if lpipers > 1 else "", "s" if lpipers == 1 else "")
@ -2364,52 +2293,32 @@ def chk_win_conditions(lpl, lwolves, lcubs, lrealwolves, lmonsters, ldemoniacs,
s = "s" if lmonsters > 1 else ""
message = messages["monster_win"].format(s, "" if s else "s")
winner = "monsters"
else:
message = messages["villager_win"]
winner = "villagers"
elif lwolves == lpl / 2:
if lmonsters > 0:
s = "s" if lmonsters > 1 else ""
message = messages["monster_wolf_win"].format(s)
winner = "monsters"
else:
message = messages["wolf_win"]
winner = "wolves"
elif lwolves > lpl / 2:
if lmonsters > 0:
s = "s" if lmonsters > 1 else ""
message = messages["monster_wolf_win"].format(s)
winner = "monsters"
else:
message = messages["wolf_win"]
winner = "wolves"
elif lrealwolves == 0:
chk_traitor(cli)
# update variables for recursive call (this shouldn't happen when checking 'random' role attribution, where it would probably fail)
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"}
else:
wcroles = var.WOLFCHAT_ROLES
wolves = set(list_players(wcroles))
lwolves = len(wolves)
lcubs = len(var.ROLES.get("wolf cub", ()))
lrealwolves = len(list_players(var.WOLF_ROLES - {"wolf cub"}))
ltraitors = len(var.ROLES.get("traitor", ()))
if var.PHASE == "day":
pl = set(list_players()) - (var.WOUNDED | var.CONSECRATING)
evt = Event("get_voters", {"voters": pl})
evt.dispatch(cli, var)
pl = evt.data["voters"]
lpl = len(pl)
lwolves = len(wolves & pl)
return chk_win_conditions(lpl, lwolves, lcubs, lrealwolves, lmonsters, ldemoniacs, ltraitors, lpipers, lsuccubi, lentranced, cli, end_game)
# Priorities:
# 0 = fool, other roles that end game immediately
# 1 = things that could short-circuit game ending, such as cub growing up or traitor turning
# Such events should also set stop_processing and prevent_default to True to force a re-calcuation
# 2 = win stealers not dependent on winners, such as succubus
# Events in priority 3 and 4 should check if a winner was already set and short-circuit if so
# it is NOT recommended that events in priorities 0 and 2 set stop_processing to True, as doing so
# will prevent gamemode-specific win conditions from happening
# 3 = normal roles
# 4 = win stealers dependent on who won, such as demoniac and monster
# (monster's message changes based on who would have otherwise won)
# 5 = gamemode-specific win conditions
event = Event("chk_win", {"winner": winner, "message": message, "additional_winners": None})
event.dispatch(var, lpl, lwolves, lrealwolves)
if not event.dispatch(cli, var, rolemap, lpl, lwolves, lrealwolves):
return chk_win_conditions(cli, rolemap, end_game, winner)
winner = event.data["winner"]
message = event.data["message"]
@ -2417,34 +2326,7 @@ def chk_win_conditions(lpl, lwolves, lcubs, lrealwolves, lmonsters, ldemoniacs,
return False
if end_game:
if event.data["additional_winners"] is None:
players = []
else:
players = ["{0} ({1})".format(x, get_role(x)) for x in event.data["additional_winners"]]
if winner == "monsters":
for plr in var.ROLES["monster"]:
players.append("{0} ({1})".format(plr, get_role(plr)))
elif winner == "demoniacs":
for plr in var.ROLES["demoniac"]:
players.append("{0} ({1})".format(plr, get_role(plr)))
elif winner == "wolves":
for plr in list_players(var.WOLFTEAM_ROLES):
players.append("{0} ({1})".format(plr, get_role(plr)))
elif winner == "villagers":
# There is a regression issue in the 3.3 collections module where OrderedDict.keys is not set-like
# this was fixed in later releases, and since development and main instance are on 3.4 or 3.5, this was not noticed
# collections.OrderedDict being a dict subclass, dict methods all work. Thus, we can pass the instance to dict.keys and be done with it (since it's set-like)
vroles = (role for role in var.ROLES.keys() if var.ROLES[role] and role not in (var.WOLFTEAM_ROLES | var.TRUE_NEUTRAL_ROLES | dict.keys(var.TEMPLATE_RESTRICTIONS)))
for plr in list_players(vroles):
players.append("{0} ({1})".format(plr, get_role(plr)))
elif winner == "pipers":
for plr in var.ROLES["piper"]:
players.append("{0} ({1})".format(plr, get_role(plr)))
elif winner == "succubi":
for plr in var.ROLES["succubus"] | var.ENTRANCED:
players.append("{0} ({1})".format(plr, get_role(plr)))
debuglog("WIN:", winner)
debuglog("PLAYERS:", ", ".join(players))
cli.msg(chan, message)
stop_game(cli, winner, additional_winners=event.data["additional_winners"])
return True
@ -2826,11 +2708,6 @@ def del_player(cli, nick, forced_death=False, devoice=True, end_game=True, death
del x[k]
if nick in var.DISCONNECTED:
del var.DISCONNECTED[nick]
if nickrole == "succubus" and not var.ROLES["succubus"]:
while var.ENTRANCED:
entranced = var.ENTRANCED.pop()
pm(cli, entranced, messages["entranced_revert_win"])
var.ENTRANCED_DYING.clear() # for good measure
if var.PHASE == "night":
# remove players from night variables
# the dicts are handled above, these are the lists of who has acted which is used to determine whether night should end
@ -3093,9 +2970,6 @@ def rename_player(var, user, prefix):
nick = user.nick
if var.PHASE in var.GAME_PHASES:
if prefix in var.ENTRANCED: # need to update this after death, too
var.ENTRANCED.remove(prefix)
var.ENTRANCED.add(nick)
if prefix in var.SPECTATING_WOLFCHAT:
var.SPECTATING_WOLFCHAT.remove(prefix)
var.SPECTATING_WOLFCHAT.add(nick)
@ -3199,7 +3073,7 @@ def rename_player(var, user, prefix):
var.JESTERS, var.AMNESIACS, var.LYCANTHROPES, var.LUCKY, var.DISEASED,
var.MISDIRECTED, var.EXCHANGED, var.IMMUNIZED, var.CURED_LYCANS,
var.ALPHA_WOLVES, var.CURSED, var.CHARMERS, var.CHARMED, var.TOBECHARMED,
var.PRIESTS, var.CONSECRATING, var.ENTRANCED_DYING, var.DYING):
var.PRIESTS, var.CONSECRATING, var.DYING):
if prefix in setvar:
setvar.remove(prefix)
setvar.add(nick)
@ -3450,7 +3324,6 @@ def begin_day(cli):
var.LUCKY = set()
var.DISEASED = set()
var.MISDIRECTED = set()
var.ENTRANCED_DYING = set()
var.DYING = set()
msg = messages["villagers_lynch"].format(botconfig.CMD_CHAR, len(list_players()) // 2 + 1)
@ -3643,9 +3516,6 @@ def transition_day(cli, gameid=0):
bitten = evt.data["bitten"]
numkills = evt.data["numkills"]
for v in var.ENTRANCED_DYING:
var.DYING.add(v)
for player in var.DYING:
victims.append(player)
onlybywolves.discard(player)
@ -3802,7 +3672,7 @@ def transition_day(cli, gameid=0):
for victim in vlist:
if not revt.dispatch(cli, var, victim):
continue
if victim in var.ROLES["harlot"] | var.ROLES["succubus"] and var.HVISITED.get(victim) and victim not in revt.data["dead"] and victim in revt.data["onlybywolves"]:
if victim in var.ROLES["harlot"] and var.HVISITED.get(victim) and victim not in revt.data["dead"] and victim in revt.data["onlybywolves"]:
# alpha wolf can bite a harlot visiting another wolf, don't play a message in that case
# kept as a nested if so that the other victim logic does not run
if victim not in revt.data["bitten"]:
@ -3863,6 +3733,17 @@ def transition_day(cli, gameid=0):
out = out.strip() # remove surrounding whitespace
revt.data["message"].append(out)
# Priorities:
# 1 = harlot/succubus visiting victim
# 2 = determining whether or not we should print the "no victims" message
# 3 = harlot visiting wolf
# 4 = gunner shooting wolf
# 5 = wolves killing diseased, wolves stealing gun
# 10 = alpha wolf bite
# Note that changing the "novictmsg" data item only makes sense for priority 1 events,
# as after that point the message was already added. Events that could kill more people
# should do so before priority 10. Events that require everyone that can be killed to
# be listed as dead should be priority 10 or later.
revt2 = Event("transition_day_resolve_end", {
"message": revt.data["message"],
"novictmsg": revt.data["novictmsg"],
@ -4018,7 +3899,7 @@ def chk_nightdone(cli):
actedcount = sum(map(len, (var.HVISITED, var.PASSED, var.OBSERVED,
var.HEXED, var.CURSED, var.CHARMERS)))
nightroles = get_roles("harlot", "succubus", "sorcerer", "hag", "warlock", "werecrow", "piper", "prophet")
nightroles = get_roles("harlot", "sorcerer", "hag", "warlock", "werecrow", "piper", "prophet")
for nick, info in var.PRAYED.items():
if info[0] > 0:
@ -4475,6 +4356,7 @@ def shoot(var, wrapper, message):
else:
chances = var.GUN_CHANCES
# TODO: make this into an event once we split off gunner
if victim in var.ROLES["succubus"]:
chances = chances[:3] + (0,)
@ -4527,8 +4409,9 @@ def shoot(var, wrapper, message):
if not del_player(wrapper.source.client, wrapper.source.nick, killer_role="villager"): # blame explosion on villager's shoddy gun construction or something
return # Someone won.
def is_safe(nick, victim): # helper function
return nick in var.ENTRANCED and victim in var.ROLES["succubus"]
def is_safe(nick, victim): # replace calls to this with targeted_command event when splitting roles
from src.roles import succubus
return nick in succubus.ENTRANCED and victim in var.ROLES["succubus"]
@cmd("bless", chan=False, pm=True, playing=True, silenced=True, phases=("day",), roles=("priest",))
def bless(cli, nick, chan, rest):
@ -4740,16 +4623,13 @@ def pray(cli, nick, chan, rest):
chk_nightdone(cli)
@cmd("visit", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("harlot", "succubus"))
@cmd("visit", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("harlot",))
def hvisit(cli, nick, chan, rest):
"""Visit a player. You will die if you visit a wolf or a target of the wolves."""
role = get_role(nick)
if var.HVISITED.get(nick):
if role == "harlot":
pm(cli, nick, messages["harlot_already_visited"].format(var.HVISITED[nick]))
else:
pm(cli, nick, messages["succubus_already_visited"].format(var.HVISITED[nick]))
pm(cli, nick, messages["harlot_already_visited"].format(var.HVISITED[nick]))
return
victim = get_victim(cli, nick, re.split(" +",rest)[0], False, True)
if not victim:
@ -4760,64 +4640,18 @@ def hvisit(cli, nick, chan, rest):
return
else:
victim = choose_target(nick, victim)
if role != "succubus" and check_exchange(cli, nick, victim):
if check_exchange(cli, nick, victim):
return
var.HVISITED[nick] = victim
if role == "harlot":
pm(cli, nick, messages["harlot_success"].format(victim))
if nick != victim: #prevent luck/misdirection totem weirdness
pm(cli, victim, messages["harlot_success"].format(nick))
if get_role(victim) == "succubus":
pm(cli, nick, messages["notify_succubus_target"].format(victim))
pm(cli, victim, messages["succubus_harlot_success"].format(nick))
var.ENTRANCED.add(nick)
else:
if get_role(victim) != "succubus":
var.ENTRANCED.add(victim)
pm(cli, nick, messages["succubus_target_success"].format(victim))
if nick != victim:
pm(cli, victim, (messages["notify_succubus_target"]).format(nick))
if var.TARGETED.get(victim) in var.ROLES["succubus"]:
msg = messages["no_target_succubus"].format(var.TARGETED[victim])
del var.TARGETED[victim]
if victim in var.ROLES["village drunk"]:
target = random.choice(list(set(list_players()) - var.ROLES["succubus"] - {victim}))
msg += messages["drunk_target"].format(target)
var.TARGETED[victim] = target
pm(cli, victim, nick)
# temp hack, will do something better once succubus is split off
from src.roles import wolf, hunter, dullahan, vigilante, shaman
if (shaman.SHAMANS.get(victim, (None, None))[1] in var.ROLES["succubus"] and
(get_role(victim) == "crazed shaman" or shaman.TOTEMS[victim] not in var.BENEFICIAL_TOTEMS)):
pm(cli, victim, messages["retract_totem_succubus"].format(shaman.SHAMANS[victim]))
del shaman.SHAMANS[victim]
if victim in var.HEXED and var.LASTHEXED[victim] in var.ROLES["succubus"]:
pm(cli, victim, messages["retract_hex_succubus"].format(var.LASTHEXED[victim]))
var.TOBESILENCED.remove(nick)
var.HEXED.remove(victim)
del var.LASTHEXED[victim]
if set(wolf.KILLS.get(victim, ())) & var.ROLES["succubus"]:
for s in var.ROLES["succubus"]:
if s in wolf.KILLS[victim]:
pm(cli, victim, messages["no_kill_succubus"].format(nick))
wolf.KILLS[victim].remove(s)
if not wolf.KILLS[victim]:
del wolf.KILLS[victim]
if hunter.KILLS.get(victim) in var.ROLES["succubus"]:
pm(cli, victim, messages["no_kill_succubus"].format(hunter.KILLS[victim]))
del hunter.KILLS[victim]
hunter.HUNTERS.discard(victim)
if vigilante.KILLS.get(victim) in var.ROLES["succubus"]:
pm(cli, victim, messages["no_kill_succubus"].format(vigilante.KILLS[victim]))
del vigilante.KILLS[victim]
if var.BITE_PREFERENCES.get(victim) in var.ROLES["succubus"]:
pm(cli, victim, messages["no_kill_succubus"].format(var.BITE_PREFERENCES[victim]))
del var.BITE_PREFERENCES[victim]
if dullahan.TARGETS.get(victim, set()) & var.ROLES["succubus"]:
pm(cli, victim, messages["dullahan_no_kill_succubus"])
if dullahan.KILLS.get(victim) in var.ROLES["succubus"]:
del dullahan.KILLS[victim]
pm(cli, nick, messages["harlot_success"].format(victim))
if nick != victim: #prevent luck/misdirection totem weirdness
pm(cli, victim, messages["harlot_success"].format(nick))
# TODO: turn this into an event once harlot is split off
if get_role(victim) == "succubus":
pm(cli, nick, messages["notify_succubus_target"].format(victim))
pm(cli, victim, messages["succubus_harlot_success"].format(nick))
from src.roles import succubus
succubus.ENTRANCED.add(nick)
debuglog("{0} ({1}) VISIT: {2} ({3})".format(nick, role, victim, get_role(victim)))
chk_nightdone(cli)
@ -4896,7 +4730,7 @@ def bite_cmd(cli, nick, chan, rest):
chk_nightdone(cli)
@cmd("pass", chan=False, pm=True, playing=True, phases=("night",),
roles=("harlot", "turncoat", "warlock", "succubus"))
roles=("harlot", "turncoat", "warlock"))
def pass_cmd(cli, nick, chan, rest): # XXX: hvisit (3 functions above this one) also needs updating alongside this
"""Decline to use your special power for that night."""
nickrole = get_role(nick)
@ -4915,12 +4749,6 @@ def pass_cmd(cli, nick, chan, rest): # XXX: hvisit (3 functions above this one)
return
var.HVISITED[nick] = None
pm(cli, nick, messages["no_visit"])
elif nickrole == "succubus":
if var.HVISITED.get(nick):
pm(cli, nick, messages["succubus_already_visited"].format(var.HVISITED[nick]))
return
var.HVISITED[nick] = None
pm(cli, nick, messages["succubus_pass"])
elif nickrole == "turncoat":
if var.TURNCOATS[nick][1] == var.NIGHT_COUNT:
# theoretically passing would revert them to how they were before, but
@ -5242,25 +5070,12 @@ def charm(cli, nick, chan, rest):
@event_listener("targeted_command", priority=9)
def on_targeted_command(evt, cli, var, cmd, actor, orig_target, tags):
# TODO: move beneficial command check into roles/succubus.py
if "beneficial" not in tags and is_safe(actor, evt.data["target"]):
try:
what = evt.params.action
except AttributeError:
what = cmd
pm(cli, actor, messages["no_acting_on_succubus"].format(what))
evt.stop_processing = True
evt.prevent_default = True
return
if evt.data["misdirection"]:
evt.data["target"] = choose_target(actor, evt.data["target"])
if evt.data["exchange"] and check_exchange(cli, actor, evt.data["target"]):
evt.stop_processing = True
evt.prevent_default = True
return
@hook("featurelist") # For multiple targets with PRIVMSG
def getfeatures(cli, nick, *rest):
@ -5475,8 +5290,10 @@ def transition_night(cli):
var.ROLES[amnrole].add(amn)
var.AMNESIACS.add(amn)
var.FINAL_ROLES[amn] = amnrole
if amnrole == "succubus" and amn in var.ENTRANCED:
var.ENTRANCED.remove(amn)
# TODO: turn into event when amnesiac is split
from src.roles import succubus
if amnrole == "succubus" and amn in succubus.ENTRANCED:
succubus.ENTRANCED.remove(amn)
pm(cli, amn, messages["no_longer_entranced"])
if var.FIRST_NIGHT: # we don't need to tell them twice if they remember right away
continue
@ -5540,16 +5357,6 @@ def transition_night(cli):
else:
pm(cli, drunk, messages["drunk_simple"])
for succubus in var.ROLES["succubus"]:
pl = ps[:]
random.shuffle(pl)
pl.remove(succubus)
if succubus in var.PLAYERS and not is_user_simple(succubus):
pm(cli, succubus, messages["succubus_notify"])
else:
pm(cli, succubus, messages["succubus_simple"])
pm(cli, succubus, "Players: " + ", ".join(("{0} ({1})".format(x, get_role(x)) if x in var.ROLES["succubus"] else x for x in pl)))
for ms in var.ROLES["mad scientist"]:
pl = ps[:]
for index, user in enumerate(var.ALL_PLAYERS):
@ -5982,8 +5789,6 @@ def start(cli, nick, chan, forced = False, restart = ""):
var.EXTRA_WOLVES = 0
var.PRIESTS = set()
var.CONSECRATING = set()
var.ENTRANCED = set()
var.ENTRANCED_DYING = set()
var.DYING = set()
var.PRAYED = {}
@ -7327,12 +7132,6 @@ if botconfig.DEBUG_MODE or botconfig.ALLOWED_NORMAL_MODE_COMMANDS:
if var.IMMUNIZED:
output.append("\u0002immunized\u0002: {0}".format(", ".join(var.IMMUNIZED)))
if var.ENTRANCED:
output.append("\u0002entranced players\u0002: {0}".format(", ".join(var.ENTRANCED)))
if var.ENTRANCED_DYING:
output.append("\u0002dying entranced players\u0002: {0}".format(", ".join(var.ENTRANCED_DYING)))
# get charmed players
if var.CHARMED | var.TOBECHARMED:
output.append("\u0002charmed players\u0002: {0}".format(", ".join(var.CHARMED | var.TOBECHARMED)))