diff --git a/src/gamemodes.py b/src/gamemodes.py index 2af9d5c..e372ade 100644 --- a/src/gamemodes.py +++ b/src/gamemodes.py @@ -514,10 +514,12 @@ class RandomMode(GameMode): lcubs = addroles["wolf cub"] lrealwolves = sum(addroles[r] for r in var.WOLF_ROLES - {"wolf cub"}) lmonsters = addroles["monster"] + ldemoniacs = addroles["demoniacs"] ltraitors = addroles["traitor"] lpipers = addroles["piper"] + lsuccubi = addroles["succubus"] - if chk_win_conditions(lpl, lwolves, lcubs, lrealwolves, lmonsters, ltraitors, lpipers, cli, end_game=False): + if chk_win_conditions(lpl, lwolves, lcubs, lrealwolves, lmonsters, ldemoniacs, ltraitors, lpipers, lsuccubi, 0, cli, end_game=False): return self.role_attribution(evt, cli, chk_win_conditions, var, villagers) evt.prevent_default = True diff --git a/src/settings.py b/src/settings.py index f6b07eb..4c5a0d7 100644 --- a/src/settings.py +++ b/src/settings.py @@ -260,6 +260,9 @@ SEEN_WOLF = WOLF_ROLES | {"monster", "mad scientist", "succubus"} SEEN_DEFAULT = frozenset({"traitor", "hag", "sorcerer", "time lord", "villager", "cultist", "minion", "turncoat", "amnesiac", "vengeful ghost", "lycan", "clone", "fool", "jester", "werekitten", "warlock", "piper", "doomsayer", "demoniac"}) +# these totems are detrimental for the *receiving* person, but can be detrimental to someone else acting on the receiver! +DETRIMENTAL_TOTEMS = frozenset({"death", "silence", "impatience", "pacifism", "narcolepsy", "exchange", "lycanthropy", "misdirection"}) + # The roles in here are considered templates and will be applied on TOP of other roles. The restrictions are a list of roles that they CANNOT be applied to # NB: if you want a template to apply to everyone, list it here but make the restrictions an empty set. Templates not listed here are considered full roles instead TEMPLATE_RESTRICTIONS = OrderedDict([ diff --git a/src/wolfgame.py b/src/wolfgame.py index 061cf99..688c44e 100644 --- a/src/wolfgame.py +++ b/src/wolfgame.py @@ -2169,6 +2169,11 @@ def chk_decision(cli, force = ""): aftermessage = None votelist = copy.deepcopy(var.VOTES) for votee, voters in votelist.items(): + if votee in var.ROLES["succubus"]: + for vtr in var.ENTRANCED: + if vtr in voters: + voters.remove(vtr) + impatient_voters = [] numvotes = 0 random.shuffle(var.IMPATIENT) @@ -2533,6 +2538,8 @@ def stop_game(cli, winner = "", abort = False, additional_winners = None): won = True elif winner == "villagers": won = True + elif winner == "succubi" and rol == "succubus": + won = True survived = var.list_players() if plr.startswith("(dced)"): @@ -2598,6 +2605,8 @@ def stop_game(cli, winner = "", abort = False, additional_winners = None): iwon = False elif rol == "jester" and splr in var.JESTERS: iwon = True + elif winner == "succubi" and plr in var.ROLES["succubi"] | var.ENTRANCED: + iwon = True elif not iwon: iwon = won and splr in survived # survived, team won = individual win @@ -2672,6 +2681,8 @@ def chk_win(cli, end_game=True, winner=None): 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) if var.PHASE == "day": for p in var.WOUNDED | var.ASLEEP | var.CONSECRATING: try: @@ -2683,9 +2694,9 @@ def chk_win(cli, end_game=True, winner=None): except KeyError: pass - return chk_win_conditions(lpl, lwolves, lcubs, lrealwolves, lmonsters, ldemoniacs, ltraitors, lpipers, cli, end_game, winner) + 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, cli, end_game=True, winner=None): +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: @@ -2696,6 +2707,11 @@ def chk_win_conditions(lpl, lwolves, lcubs, lrealwolves, lmonsters, ldemoniacs, elif lpl < 1: message = "Game over! There are no players remaining. Nobody wins." winner = "none" + elif var.PHASE == "day" and lpl - lsuccubi <= lentranced: # entranced people + winner = "succubi" + message = ("Game over! The succub{0} ha{1} won! The succub{0} then take{2} all of the " + "entranced players with them, and go{3} out of the village, never to return" + "...").format(*(("us", "s", "s", "es") if lsuccubi == 1 else ("i", "ve", "", ""))) elif var.PHASE == "day" and lpipers and len(var.list_players()) - lpipers == len(var.CHARMED - var.ROLES["piper"]): winner = "pipers" message = ("Game over! Everyone has fallen victim to the charms of the " + @@ -2747,7 +2763,7 @@ def chk_win_conditions(lpl, lwolves, lcubs, lrealwolves, lmonsters, ldemoniacs, lcubs = len(var.ROLES.get("wolf cub", ())) lrealwolves = len(var.list_players(var.WOLF_ROLES - {"wolf cub"})) ltraitors = len(var.ROLES.get("traitor", ())) - return chk_win_conditions(lpl, lwolves, lcubs, lrealwolves, lmonsters, ldemoniacs, ltraitors, lpipers, cli, end_game) + return chk_win_conditions(lpl, lwolves, lcubs, lrealwolves, lmonsters, ldemoniacs, ltraitors, lpipers, lsuccubi, lentranced, cli, end_game) event = Event("chk_win", {"winner": winner, "message": message, "additional_winners": None}) event.dispatch(var, lpl, lwolves, lrealwolves) @@ -2778,6 +2794,9 @@ def chk_win_conditions(lpl, lwolves, lcubs, lrealwolves, lmonsters, ldemoniacs, elif winner == "pipers": for plr in var.ROLES["piper"]: players.append("{0} ({1})".format(plr, var.get_role(plr))) + elif winner == "succubi": + for plr in var.ROLES["succubus"] | var.ENTRANCED: + players.append("{0} ({1})".format(plr, var.get_role(plr))) debuglog("WIN:", winner) debuglog("PLAYERS:", ", ".join(players)) cli.msg(chan, message) @@ -3621,6 +3640,9 @@ def rename_player(cli, prefix, nick): if prefix in var.CONSECRATING: var.CONSECRATING.remove(prefix) var.CONSECRATING.add(nick) + if prefix in var.ENTRANCED: + var.ENTRANCED.remove(prefix) + var.ENTRANCED.add(nick) with var.GRAVEYARD_LOCK: # to be safe if prefix in var.LAST_SAID_TIME.keys(): var.LAST_SAID_TIME[nick] = var.LAST_SAID_TIME.pop(prefix) @@ -4677,10 +4699,11 @@ def chk_nightdone(cli): var.OTHER_KILLS, var.PASSED, var.OBSERVED, var.HEXED, var.SHAMANS, var.CURSED, var.CHARMERS))) - nightroles = get_roles("seer", "oracle", "harlot", "bodyguard", "guardian angel", - "wolf", "werecrow", "alpha wolf", "sorcerer", "hunter", - "hag", "shaman", "crazed shaman", "augur", "werekitten", - "warlock", "piper", "wolf mystic", "fallen angel") + nightroles = get_roles("seer", "oracle", "harlot", "succubus", "bodyguard", + "guardian angel", "wolf", "werecrow", "alpha wolf", + "sorcerer", "hunter", "hag", "shaman", "crazed shaman", + "augur", "werekitten", "warlock", "piper", "wolf mystic", + "fallen angel") for ghost, against in var.VENGEFUL_GHOSTS.items(): if not against.startswith("!"): @@ -5335,7 +5358,8 @@ def shoot(cli, nick, chan, rest): if not del_player(cli, 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"] @cmd("kill", chan=False, pm=True, phases=("night",)) def kill(cli, nick, chan, rest): @@ -5388,10 +5412,16 @@ def kill(cli, nick, chan, rest): victim = get_victim(cli, nick, victim, False) if not victim: return + if is_safe(nick, victim): + pm(cli, nick, "You may not target a succubus.") + return if victim2 != None: victim2 = get_victim(cli, nick, victim2, False) if not victim2: return + if is_safe(nick, victim): + pm(cli, nick, "You may not target a succubus.") + return if victim == nick or victim2 == nick: if nick in var.VENGEFUL_GHOSTS.keys(): @@ -5760,9 +5790,11 @@ def pray(cli, nick, chan, rest): chk_nightdone(cli) -@cmd("visit", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("harlot",)) +@cmd("visit", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("harlot", "succubus")) def hvisit(cli, nick, chan, rest): """Visit a player. You will die if you visit a wolf or a target of the wolves.""" + role = var.get_role(nick) + if var.HVISITED.get(nick): pm(cli, nick, ("You are already spending the night "+ "with \u0002{0}\u0002.").format(var.HVISITED[nick])) @@ -5779,12 +5811,45 @@ def hvisit(cli, nick, chan, rest): if check_exchange(cli, nick, victim): return var.HVISITED[nick] = victim - pm(cli, nick, ("You are spending the night with \u0002{0}\u0002. "+ - "Have a good time!").format(victim)) - if nick != victim: #prevent luck/misdirection totem weirdness - pm(cli, victim, ("You are spending the night with \u0002{0}"+ - "\u0002. Have a good time!").format(nick)) - debuglog("{0} ({1}) VISIT: {2} ({3})".format(nick, var.get_role(nick), victim, var.get_role(victim))) + if role == "harlot": + pm(cli, nick, ("You are spending the night with \u0002{0}\u0002. "+ + "Have a good time!").format(victim)) + if nick != victim: #prevent luck/misdirection totem weirdness + pm(cli, victim, ("You are spending the night with \u0002{0}"+ + "\u0002. Have a good time!").format(nick)) + else: + var.ENTRANCED.add(victim) + pm(cli, nick, "You are entrancing \u0002{0}\u0002 tonight.".format(victim)) + if nick != victim: + pm(cli, victim, ("You are being 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 along them if they are alive.").format(nick)) + + if var.OTHER_KILLS.get(victim) == nick: + pm(cli, victim, "You have retracted your kill on \u0002{0}\u0002.".format(nick)) # placeholder + del var.OTHER_KILLS[victim] + if var.TARGETED.get(victim) == nick: + pm(cli, victim, "You have retracted your targetting on \u0002{0}\u0002.".format(nick)) + del var.TARGETED[victim] + if var.SHAMANS.get(victim) == nick and var.TOTEMS[victim] in var.DETRIMENTAL_TOTEMS: + pm(cli, victim, "You have retracted your totem on \u0002{0}\u0002.".format(nick)) + del var.SHAMANS[victim] + if victim in var.HEXED and var.LASTHEXED[victim] == nick: + pm(cli, victim, "You have retracted your hex on \u0002{0}\u0002.".format(nick)) + var.TOBESILENCED.remove(nick) + var.HEXED.remove(victim) + del var.LASTHEXED[victim] + if nick in var.KILLS.get(victim, ()): + pm(cli, victim, "You have retracted your kill on \u0002{0}\u0002.".format(nick)) + var.KILLS[victim].remove(nick) + if not var.KILLS[victim]: + del var.KILLS[victim] + if var.BITE_PREFERENCES.get(victim) == nick: + pm(cli, victim, "You are retracted your bite on \u0002{0}\u0002.".format(nick)) + del var.BITE_PREFERENCES[victim] + + debuglog("{0} ({1}) VISIT: {2} ({3})".format(nick, role, victim, var.get_role(victim))) chk_nightdone(cli) def is_fake_nick(who): @@ -5870,6 +5935,9 @@ def totem(cli, nick, chan, rest, prefix="You"): role = var.get_role(nick) if role != "crazed shaman": totem = " of " + var.TOTEMS[nick] + if is_safe(nick, victim) and var.TOTEMS[nick] in var.DETRIMENTAL_TOTEMS: + pm(cli, nick, "You may not give a totem{0} to a succubus.".format(totem)) + return pm(cli, nick, ("{0} have given a totem{1} to \u0002{2}\u0002.").format(prefix, totem, original_victim)) var.SHAMANS[nick] = (victim, original_victim) debuglog("{0} ({1}) TOTEM: {2} ({3})".format(nick, role, victim, totem)) @@ -5976,6 +6044,9 @@ def bite_cmd(cli, nick, chan, rest): if not victim: pm(cli, nick, "Please choose who to bite by specifying their nick.") return + if is_safe(nick, victim): + pm(cli, nick, "You may not bite a succubus.") + return vrole = var.get_role(victim) actual = choose_target(nick, victim) @@ -6179,6 +6250,10 @@ def target(cli, nick, chan, rest): pm(cli, nick, "You may not target yourself.") return + if is_safe(nick, victim): + pm(cli, nick, "You may not target a succubus.") + 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 @@ -6205,6 +6280,10 @@ def hex_target(cli, nick, chan, rest): "You cannot hex the same person two nights in a row.").format(victim)) return + if is_safe(nick, victim): + pm(cli, nick, "You may not hex a succubus.") + return + victim = choose_target(nick, victim) if check_exchange(cli, nick, victim): return @@ -6245,6 +6324,9 @@ def curse(cli, nick, chan, rest): victim = get_victim(cli, nick, re.split(" +",rest)[0], False) if not victim: return + if is_safe(nick, victim): + pm(cli, nick, "You may not curse a succubus.") + return # There may actually be valid strategy in cursing other wolfteam members, # but for now it is not allowed. If someone seems suspicious and shows as @@ -6323,10 +6405,15 @@ def charm(cli, nick, chan, rest): victim = get_victim(cli, nick, victim, False, True) if not victim: return + if is_safe(nick, victim): + pm(cli, nick, "You may not charm a succubus.") + return if victim2 is not None: victim2 = get_victim(cli, nick, victim2, False, True) if not victim2: return + if is_safe(nick, victim): + pm(cli, nick, "You may not charm a succubus.") if victim == victim2: pm(cli, nick, "You must choose two different people.") @@ -7479,6 +7566,8 @@ def start(cli, nick, chan, forced = False, restart = ""): var.BLESSED = set() var.PRIESTS = set() var.CONSECRATING = set() + var.ENTRANCED = set() + var.ENTRANCED_DYING = set() var.DEADCHAT_PLAYERS = set()