From 9b61dde15eba63d86af587000f0b855f2f3affed Mon Sep 17 00:00:00 2001 From: skizzerz Date: Sat, 13 Dec 2014 17:31:34 -0600 Subject: [PATCH] Add 2 new roles: doctor and alpha wolf Doctor is on the village team, and has a number of immunizations they can give out during the day. An immunized lycan reverts a normal villager, and anyone that is immunized will die instead of turning into a wolf if they are bitten by the alpha wolf. Beware, however, because attempting to immunize someone already bitten will simply speed up the process of them becoming a wolf! Alpha wolf has a once-per-game ability where they can bite the wolves' target instead of killing them following the death of a werewolf during the day. The bitten person will subsequently turn into a wolf in 3 nights. --- modules/wolfgame.py | 513 +++++++++++++++++++++++++++++++++++-------- settings/wolfgame.py | 25 ++- 2 files changed, 437 insertions(+), 101 deletions(-) diff --git a/modules/wolfgame.py b/modules/wolfgame.py index 771b012..132720b 100644 --- a/modules/wolfgame.py +++ b/modules/wolfgame.py @@ -91,6 +91,9 @@ var.LOGGER = WolfgameLogger(var.LOG_FILENAME, var.BARE_LOG_FILENAME) var.OPPED = False # Keeps track of whether the bot is opped +var.BITTEN = {} +var.BITTEN_ROLES = {} + if botconfig.DEBUG_MODE: var.NIGHT_TIME_LIMIT = 0 # 120 var.NIGHT_TIME_WARN = 0 # 90 @@ -1061,12 +1064,6 @@ def stats(cli, nick, chan, rest): rs.append(var.DEFAULT_ROLE) - firstcount = len(var.ROLES[rs[0]]) - if firstcount > 1 or not firstcount: - vb = "are" - else: - vb = "is" - amn_roles = {"amnesiac": 0} for amn in var.ORIGINAL_ROLES["amnesiac"]: if amn not in pl: @@ -1086,6 +1083,14 @@ def stats(cli, nick, chan, rest): else: amn_roles[amnrole] = -1 + bitten_roles = {} + for bitten, role in var.BITTEN_ROLES.items(): + if role in bitten_roles: + bitten_roles[role] += 1 + else: + bitten_roles[role] = 1 + + vb = "are" for role in rs: # only show actual roles if role in ("village elder", "time lord", "vengeful ghost") or role in var.TEMPLATE_RESTRICTIONS.keys(): @@ -1093,18 +1098,43 @@ def stats(cli, nick, chan, rest): count = len(var.ROLES[role]) if role == "traitor" and var.HIDDEN_TRAITOR: continue + elif role == "lycan": + count += len([p for p in var.CURED_LYCANS if p in var.ROLES["villager"]]) + count += bitten_roles["lycan"] if "lycan" in bitten_roles else 0 elif role == var.DEFAULT_ROLE: if var.HIDDEN_TRAITOR: count += len(var.ROLES["traitor"]) + count += bitten_roles["traitor"] if "traitor" in bitten_roles else 0 if var.DEFAULT_ROLE == "villager": count += len(var.ROLES["village elder"] + var.ROLES["time lord"] + var.ROLES["vengeful ghost"]) + count -= len([p for p in var.CURED_LYCANS if p in var.ROLES["villager"]]) + count += bitten_roles["village elder"] if "village elder" in bitten_roles else 0 + count += bitten_roles["time lord"] if "time lord" in bitten_roles else 0 + count += bitten_roles["vengeful ghost"] if "vengeful ghost" in bitten_roles else 0 else: count += len(var.ROLES["vengeful ghost"]) + count += bitten_roles["vengeful ghost"] if "vengeful ghost" in bitten_roles else 0 + count += bitten_roles[var.DEFAULT_ROLE] if var.DEFAULT_ROLE in bitten_roles else 0 elif role == "villager": count += len(var.ROLES["village elder"] + var.ROLES["time lord"]) + count -= len([p for p in var.CURED_LYCANS if p in var.ROLES["villager"]]) + count += bitten_roles["villager"] if "villager" in bitten_roles else 0 + count += bitten_roles["village elder"] if "village elder" in bitten_roles else 0 + count += bitten_roles["time lord"] if "time lord" in bitten_roles else 0 + elif role == "wolf": + count -= sum(bitten_roles.values()) + else: + count += bitten_roles[role] if role in bitten_roles else 0 + if role in amn_roles: count += amn_roles[role] + if role == rs[0]: + if count == 1: + vb = "is" + else: + vb = "are" + if count > 1 or count == 0: if count == 0 and len(var.ORIGINAL_ROLES[role]) == 0: continue @@ -1956,6 +1986,8 @@ def del_player(cli, nick, forced_death = False, devoice = True, end_game = True, "to exact your revenge on the \u0002{0}\u0002 that killed you.").format(var.VENGEFUL_GHOSTS[nick])) if nickrole == "wolf cub": var.ANGRY_WOLVES = True + if nickrole in var.WOLF_ROLES and var.GAMEPHASE == "day": + var.ALPHA_ENABLED = True 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 @@ -2374,9 +2406,7 @@ def on_nick(cli, oldnick, nick): if prefix == k: var.PLAYERS[nick] = var.PLAYERS[k] del var.PLAYERS[k] - if prefix in var.GUNNERS.keys(): - var.GUNNERS[nick] = var.GUNNERS.pop(prefix) - for dictvar in (var.HVISITED, var.OBSERVED, var.GUARDED, var.OTHER_KILLS, var.TARGETED, var.CLONED, var.LASTGUARDED, var.LASTGIVEN, var.LASTHEXED): + for dictvar in (var.HVISITED, var.OBSERVED, var.GUARDED, var.OTHER_KILLS, var.TARGETED, var.CLONED, var.LASTGUARDED, var.LASTGIVEN, var.LASTHEXED, var.BITE_PREFERENCES, var.BITTEN_ROLES): kvp = [] for a,b in dictvar.items(): if a == prefix: @@ -2387,7 +2417,7 @@ def on_nick(cli, oldnick, nick): dictvar.update(kvp) if prefix in dictvar.keys(): del dictvar[prefix] - for dictvar in (var.VENGEFUL_GHOSTS, var.TOTEMS, var.FINAL_ROLES): + for dictvar in (var.VENGEFUL_GHOSTS, var.TOTEMS, var.FINAL_ROLES, var.BITTEN, var.GUNNERS, var.DOCTORS): if prefix in dictvar.keys(): dictvar[nick] = dictvar[prefix] del dictvar[prefix] @@ -2498,6 +2528,15 @@ def on_nick(cli, oldnick, nick): if prefix in var.TOBEEXCHANGED: var.TOBEEXCHANGED.remove(prefix) var.TOBEEXCHANGED.append(nick) + if prefix in var.IMMUNIZED: + var.IMMUNIZED.remove(prefix) + var.IMMUNIZED.add(nick) + if prefix in var.CURED_LYCANS: + var.CURED_LYCANS.remove(prefix) + var.CURED_LYCANS.append(nick) + if prefix in var.ALPHA_WOLVES: + var.ALPHA_WOLVES.remove(prefix) + var.ALPHA_WOLVES.append(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) @@ -2823,6 +2862,13 @@ def transition_day(cli, gameid=0): var.NIGHT_TIMEDELTA += td min, sec = td.seconds // 60, td.seconds % 60 + # determine if we need to play the new wolf message due to bitten people + new_wolf = False + for (p, v) in var.BITTEN.items(): + if v <= 0: + new_wolf = True + break + found = {} for v in var.KILLS.values(): for p in v: @@ -2833,6 +2879,7 @@ def transition_day(cli, gameid=0): maxc = 0 victims = [] + bitten = [] killers = {} # dict of victim: list of killers (for retribution totem) bywolves = set() # wolves targeted, others may have as well (needed for harlot visit and maybe other things) onlybywolves = set() # wolves and nobody else targeted (needed for lycan) @@ -2875,6 +2922,37 @@ def transition_day(cli, gameid=0): else: killers[victim] = ["@wolves"] + if var.ALPHA_ENABLED: # check for bites + for (alpha, desired) in var.BITE_PREFERENCES.items(): + if len(bywolves) == 0: + # nobody to bite, so let them do it again in the future + var.ALPHA_WOLVES.remove(alpha) + if len(bywolves) == 1: + target = tuple(bywolves)[0] + elif desired in bywolves: + target = desired + else: + target = random.choice(tuple(bywolves)) + pm(cli, alpha, "You have bitten \u0002{0}\u0002.".format(target)) + targetrole = var.get_role(target) + # do the usual checks; we can only bite those that would otherwise die + # (e.g. block it on visiting harlot, GA/bodyguard, and protection totem) + # also if a lycan is bitten, just turn them into a wolf immediately + if (target not in var.PROTECTED + and target not in var.GUARDED.values() + and (target not in var.ROLES["harlot"] or not var.HVISITED.get(target)) + and target not in var.ROLES["lycan"] + and target not in var.LYCANTHROPES + and target not in var.IMMUNIZED): + var.BITTEN[target] = var.ALPHA_WOLF_NIGHTS + bitten.append(target) + victims.remove(target) + bywolves.remove(target) + onlybywolves.remove(target) + killers[target].remove("@wolves") + + var.BITE_PREFERENCES = {} + for monster in var.ROLES["monster"]: if monster in victims: victims.remove(monster) @@ -2975,6 +3053,7 @@ def transition_day(cli, gameid=0): # This needs to go down here since having them be their night value matters above var.ANGRY_WOLVES = False var.DISEASED_WOLVES = False + var.ALPHA_ENABLED = False dead = [] for crow, target in iter(var.OBSERVED.items()): @@ -2990,6 +3069,10 @@ def transition_day(cli, gameid=0): vlist = copy.copy(victims) novictmsg = True + if new_wolf: + message.append("A chilling howl was heard last night. It appears there is another werewolf in our midst!") + novictmsg = False + for victim in vlist: if victim in var.PROTECTED and victim not in var.DYING: message.append(("\u0002{0}\u0002 was attacked last night, but their totem " + @@ -3012,7 +3095,7 @@ def transition_day(cli, gameid=0): elif victim in var.ROLES["harlot"] and var.HVISITED.get(victim) and victim not in var.DYING and victim not in dead and victim in onlybywolves: message.append("The wolves' selected victim was a harlot, who was not at home last night.") novictmsg = False - elif (victim in var.ROLES["lycan"] or victim in var.LYCANTHROPES) and victim in onlybywolves: + elif (victim in var.ROLES["lycan"] or victim in var.LYCANTHROPES) and victim in onlybywolves and victim not in var.IMMUNIZED: message.append("A chilling howl was heard last night. It appears there is another werewolf in our midst!") pm(cli, victim, 'HOOOOOOOOOWL. You have become... a wolf!') vrole = var.get_role(victim) @@ -3179,6 +3262,14 @@ def transition_day(cli, gameid=0): for msg in message: var.LOGGER.logMessage(msg.replace("\02", "")) + for chump in var.BITTEN.keys(): + if chump not in dead and var.get_role(chump) not in var.WOLF_ROLES: + pm(cli, chump, get_bitten_message(chump)) + + for chump in bitten: + if chump not in dead and chump not in var.WOLF_ROLES: + pm(cli, chump, "You woke up today feeling light-headed, and you notice some odd bite marks on your neck...") + for deadperson in dead: # kill each player, but don't end the game if one group outnumbers another # take a shortcut for killer_role here since vengeful ghost only cares about team and not particular roles # this will have to be modified to track the actual killer if that behavior changes @@ -3208,7 +3299,7 @@ def chk_nightdone(cli): list(var.TARGETED.keys())) nightroles = (var.ROLES["seer"] + var.ROLES["oracle"] + var.ROLES["harlot"] + var.ROLES["bodyguard"] + var.ROLES["guardian angel"] + var.ROLES["wolf"] + - var.ROLES["werecrow"] + var.ROLES["sorcerer"] + var.ROLES["hunter"] + + var.ROLES["werecrow"] + var.ROLES["alpha wolf"] + var.ROLES["sorcerer"] + var.ROLES["hunter"] + list(var.VENGEFUL_GHOSTS.keys()) + var.ROLES["hag"] + var.ROLES["shaman"] + var.ROLES["crazed shaman"] + var.ROLES["assassin"] + var.ROLES["augur"]) if var.FIRST_NIGHT: @@ -3216,7 +3307,7 @@ def chk_nightdone(cli): nightroles += var.ROLES["matchmaker"] + var.ROLES["clone"] if var.DISEASED_WOLVES: - nightroles = [p for p in nightroles if p not in var.ROLES["wolf"]] + nightroles = [p for p in nightroles if p not in var.ROLES["wolf"] and p not in var.ROLES["alpha wolf"]] for p in var.HUNTERS: # only remove one instance of their name if they have used hunter ability, in case they have templates @@ -3461,6 +3552,17 @@ def check_exchange(cli, actor, nick): del var.LASTHEXED[actor] if actor in var.HEXED: var.HEXED.remove(actor) + elif actor_role == "doctor": + if nick_role == "doctor": + temp_immunizations = var.DOCTORS[actor] + var.DOCTORS[actor] = var.DOCTORS[nick] + var.DOCTORS[nick] = temp_immunizations + else: + var.DOCTORS[nick] = var.DOCTORS[actor] + del var.DOCTORS[actor] + elif actor_role == "alpha wolf": + if actor in var.ALPHA_WOLVES: + var.ALPHA_WOLVES.remove(actor) if nick_role == "amnesiac": nick_role = var.FINAL_ROLES[nick] @@ -3511,6 +3613,15 @@ def check_exchange(cli, actor, nick): del var.LASTHEXED[nick] if nick in var.HEXED: var.HEXED.remove(nick) + elif nick_role == "doctor": + # Both being doctors is handled above + if actor_role != "doctor": + var.DOCTORS[actor] = var.DOCTORS[nick] + del var.DOCTORS[nick] + elif nick_role == "alpha wolf": + if nick in var.ALPHA_WOLVES: + var.ALPHA_WOLVES.remove(nick) + var.FINAL_ROLES[actor] = nick_role var.FINAL_ROLES[nick] = actor_role @@ -3518,6 +3629,10 @@ def check_exchange(cli, actor, nick): var.ROLES[actor_role].remove(actor) var.ROLES[nick_role].append(actor) var.ROLES[nick_role].remove(nick) + if actor in var.BITTEN_ROLES.keys(): + var.BITTEN_ROLES[actor] = nick_role + if nick in var.BITTEN_ROLES.keys(): + var.BITTEN_ROLES[nick] = actor_role actor_rev_role = actor_role if actor_role == "vengeful ghost": @@ -3558,10 +3673,15 @@ def check_exchange(cli, actor, nick): pl[i] = player + " (cursed)" pm(cli, actor, "Players: " + ", ".join(pl)) + angry_alpha = '' if var.DISEASED_WOLVES: pm(cli, actor, 'You are feeling ill tonight, and are unable to kill anyone.') - elif var.ANGRY_WOLVES and actor_role in ("wolf", "werecrow"): + elif var.ANGRY_WOLVES and actor_role in ("wolf", "werecrow", "alpha wolf"): pm(cli, actor, 'You are \u0002angry\u0002 tonight, and may kill two targets by using "kill and ".') + angry_alpha = ' ' + if var.ALPHA_ENABLED and actor_role == "alpha wolf" and actor not in var.ALPHA_WOLVES: + pm(cli, actor, ('You may use "bite{0}" tonight in order to turn the wolves\' target into a wolf instead of killing them. ' + + 'They will turn into a wolf in {1} night{2}.').format(angry_alpha, var.ALPHA_WOLF_NIGHTS, 's' if var.ALPHA_WOLF_NIGHTS > 1 else '')) elif nick_role == "minion": wolves = var.list_players(var.WOLF_ROLES) random.shuffle(wolves) @@ -3589,10 +3709,15 @@ def check_exchange(cli, actor, nick): pl[i] = player + " (cursed)" pm(cli, nick, "Players: " + ", ".join(pl)) + angry_alpha = '' if var.DISEASED_WOLVES: pm(cli, nick, 'You are feeling ill tonight, and are unable to kill anyone.') - elif var.ANGRY_WOLVES and nick_role in ("wolf", "werecrow"): + elif var.ANGRY_WOLVES and nick_role in ("wolf", "werecrow", "alpha wolf"): pm(cli, nick, 'You are \u0002angry\u0002 tonight, and may kill two targets by using "kill and ".') + angry_alpha = ' ' + if var.ALPHA_ENABLED and nick_role == "alpha wolf" and nick not in var.ALPHA_WOLVES: + pm(cli, nick, ('You may use "bite{0}" tonight in order to turn the wolves\' target into a wolf instead of killing them. ' + + 'They will turn into a wolf in {1} night{2}.').format(angry_alpha, var.ALPHA_WOLF_NIGHTS, 's' if var.ALPHA_WOLF_NIGHTS > 1 else '')) elif actor_role == "minion": wolves = var.list_players(var.WOLF_ROLES) random.shuffle(wolves) @@ -3651,7 +3776,7 @@ def wolfretract(cli, nick, rest): return role = var.get_role(nick) - if role not in ("wolf", "werecrow", "hunter") and nick not in var.VENGEFUL_GHOSTS.keys(): + if role not in ("wolf", "werecrow", "alpha wolf", "hunter") and nick not in var.VENGEFUL_GHOSTS.keys(): return if var.PHASE != "night": pm(cli, nick, "You may only retract at night.") @@ -3664,12 +3789,23 @@ def wolfretract(cli, nick, rest): elif role == "hunter" and nick in var.HUNTERS and nick not in var.OTHER_KILLS.keys(): return - if role in var.WOLF_ROLES and nick in var.KILLS.keys(): + what = re.split(" +",rest)[0].strip().lower() + if not what or what not in ("kill", "bite"): + what = "kill" + + if what == "kill" and role in var.WOLF_ROLES and nick in var.KILLS.keys(): del var.KILLS[nick] - elif role not in var.WOLF_ROLES and nick in var.OTHER_KILLS.keys(): + pm(cli, nick, "You have retracted your kill.") + elif what == "kill" and role not in var.WOLF_ROLES and nick in var.OTHER_KILLS.keys(): del var.OTHER_KILLS[nick] var.HUNTERS.remove(nick) - pm(cli, nick, "You have retracted your vote.") + pm(cli, nick, "You have retracted your kill.") + elif what == "bite" and role == "alpha wolf" and nick in var.BITE_PREFERENCES.keys(): + del var.BITE_PREFERENCES[nick] + var.ALPHA_WOLVES.remove(nick) + pm(cli, nick, "You have decided to not bite anyone tonight.") + else: + pm(cli, nick, "You have not chosen to {0} anyone yet.".format(what)) #var.LOGGER.logBare(nick, "RETRACT", nick) @cmd("shoot") @@ -3805,9 +3941,9 @@ def kill(cli, nick, rest): role = var.get_role(nick) except KeyError: role = None - if role in var.WOLFCHAT_ROLES and role not in ("wolf", "werecrow"): + if role in var.WOLFCHAT_ROLES and role not in ("wolf", "werecrow", "alpha wolf"): return # they do this a lot. - if role not in ("wolf", "werecrow", "hunter") and nick not in var.VENGEFUL_GHOSTS.keys(): + if role not in ("wolf", "werecrow", "alpha wolf", "hunter") and nick not in var.VENGEFUL_GHOSTS.keys(): pm(cli, nick, "Only a wolf, hunter, or dead vengeful ghost may use this command.") return if var.PHASE != "night": @@ -3820,13 +3956,13 @@ def kill(cli, nick, rest): if nick in var.SILENCED: pm(cli, nick, "You have been silenced, and are unable to use any special powers.") return - if role in ("wolf", "werecrow") and var.DISEASED_WOLVES: + if role in ("wolf", "werecrow", "alpha wolf") and var.DISEASED_WOLVES: pm(cli, nick, "You are feeling ill, and are unable to kill anyone tonight.") return pieces = re.split(" +",rest) victim = pieces[0] victim2 = None - if role in ("wolf", "werecrow") and var.ANGRY_WOLVES: + if role in ("wolf", "werecrow", "alpha wolf") and var.ANGRY_WOLVES: if len(pieces) > 1: if len(pieces) > 2 and pieces[1].lower() == "and": victim2 = pieces[2] @@ -3868,7 +4004,7 @@ def kill(cli, nick, rest): pm(cli, nick, "You must target a villager.") return - if role in ("wolf", "werecrow"): + if role in ("wolf", "werecrow", "alpha wolf"): wolfchatwolves = var.list_players(var.WOLFCHAT_ROLES) if victim in wolfchatwolves or victim2 in wolfchatwolves: pm(cli, nick, "You may only kill villagers, not other wolves.") @@ -3909,7 +4045,7 @@ def kill(cli, nick, rest): else: pm(cli, nick, "You have selected \u0002{0}\u0002 to be killed.".format(victim)) var.LOGGER.logBare(nick, "SELECT", victim) - if var.ANGRY_WOLVES and role in ("wolf", "werecrow"): + if var.ANGRY_WOLVES and role in ("wolf", "werecrow", "alpha wolf"): pm(cli, nick, "You are angry tonight and may kill a second target. Use kill and to select multiple targets.") chk_nightdone(cli) @@ -4176,7 +4312,9 @@ def see(cli, nick, rest): var.LOGGER.logBare(victim, "SEEN", nick) chk_nightdone(cli) -@pmcmd("give", "totem") +# this is exactly why we should have classes; both shamans and doctors make sense to use "give" but +# a doctor could immunize someone with !totem and a shaman could totem someone with !immunize... +@pmcmd("give", "totem", "immunize", "immunise") def give(cli, nick, rest): if var.PHASE in ("none", "join"): cli.notice(nick, "No game is currently running.") @@ -4185,82 +4323,207 @@ def give(cli, nick, rest): cli.notice(nick, "You're not currently playing.") return role = var.get_role(nick) - if role not in ("shaman", "crazed shaman"): - pm(cli, nick, "Only a shaman or a crazed shaman may use this command.") + if role not in ("shaman", "crazed shaman", "doctor"): + pm(cli, nick, "Only a shaman, crazed shaman, or doctor may use this command.") return - if var.PHASE != "night": + if var.PHASE != "night" and role in ("shaman", "crazed shaman"): pm(cli, nick, "You may only give totems at night.") return + elif var.PHASE != "day" and role == "doctor": + pm(cli, nick, "You may only immunize people during the day.") + return if nick in var.SILENCED: pm(cli, nick, "You have been silenced, and are unable to use any special powers.") return if nick in var.SHAMANS: pm(cli, nick, "You have already given out your totem this round.") return + elif nick in var.DOCTORS and var.DOCTORS[nick] == 0: + pm(cli, nick, "You have run out of immunizations.") + return victim = get_victim(cli, nick, re.split(" +",rest)[0], True) if not victim: return + if role in ("shaman", "crazed shaman"): + if nick in var.LASTGIVEN and var.LASTGIVEN[nick] == victim: + pm(cli, nick, "You gave your totem to \u0002{0}\u0002 last time, you must choose someone else.".format(victim)) + return + type = "" + if role == "shaman": + type = " of " + var.TOTEMS[nick] + victim = choose_target(nick, victim) + if check_exchange(cli, nick, victim): + return + pm(cli, nick, ("You have given a totem{0} to \u0002{1}\u0002.").format(type, victim)) + totem = var.TOTEMS[nick] + if totem == "death": + if victim not in var.DYING: + var.DYING.append(victim) + elif totem == "protection": + if victim not in var.PROTECTED: + var.PROTECTED.append(victim) + elif totem == "revealing": + if victim not in var.REVEALED: + var.REVEALED.append(victim) + elif totem == "narcolepsy": + if victim not in var.ASLEEP: + var.ASLEEP.append(victim) + elif totem == "silence": + if victim not in var.TOBESILENCED: + var.TOBESILENCED.append(victim) + elif totem == "desperation": + if victim not in var.DESPERATE: + var.DESPERATE.append(victim) + elif totem == "impatience": # this totem stacks + var.IMPATIENT.append(victim) + elif totem == "pacifism": # this totem stacks + var.PACIFISTS.append(victim) + elif totem == "influence": + if victim not in var.INFLUENTIAL: + var.INFLUENTIAL.append(victim) + elif totem == "exchange": + if victim not in var.TOBEEXCHANGED: + var.TOBEEXCHANGED.append(victim) + elif totem == "lycanthropy": + if victim not in var.TOBELYCANTHROPES: + var.TOBELYCANTHROPES.append(victim) + elif totem == "luck": + if victim not in var.TOBELUCKY: + var.TOBELUCKY.append(victim) + elif totem == "pestilence": + if victim not in var.TOBEDISEASED: + var.TOBEDISEASED.append(victim) + elif totem == "retribution": + if victim not in var.RETRIBUTION: + var.RETRIBUTION.append(victim) + elif totem == "misdirection": + if victim not in var.TOBEMISDIRECTED: + var.TOBEMISDIRECTED.append(victim) + else: + pm(cli, nick, "I don't know what to do with a '{0}' totem. This is a bug, please report it to the admins.".format(totem)) + var.LASTGIVEN[nick] = victim + var.SHAMANS.append(nick) + var.LOGGER.logBare(victim, "GIVEN TOTEM", nick) + chk_nightdone(cli) + elif role == "doctor": + victim = choose_target(nick, victim) + if check_exchange(cli, nick, victim): + return + pm(cli, nick, "You have given an immunization to \u0002{0}\u0002.".format(victim)) + if var.get_role(victim) == "lycan": + lycan_message = ("You feel as if a curse has been lifted from you... It seems that your lycanthropy is cured " + + "and you will no longer become a werewolf if targeted by the wolves!") + var.ROLES["lycan"].remove(victim) + var.ROLES["villager"].append(victim) + var.FINAL_ROLES[victim] = "villager" + var.CURED_LYCANS.append(victim) + var.IMMUNIZED.add(victim) + elif victim in var.BITTEN: + # fun fact: immunizations in real life are done by injecting a small amount of (usually neutered) virus into the person + # so that their immune system can fight it off and build up antibodies. This doesn't work very well if that person is + # currently afflicted with the virus however, as you're just adding more virus to the mix... + # naturally, we would want to mimic that behavior here, and what better way of indicating that things got worse than + # by making the turning happen a night earlier? :) + var.BITTEN[victim] -= 1 + lycan_message = ("You have a brief flashback to your dream last night. " + + "The event quickly subsides, but a lingering thought remains in your mind...") + else: + lycan_message = "You don't feel any different..." + var.IMMUNIZED.add(victim) + pm(cli, victim, ("You feel a sharp prick in the back of your arm and temporarily black out. " + + "When you come to, you notice an empty syringe lying on the ground. {0}").format(lycan_message)) + var.DOCTORS[nick] -= 1 - if nick in var.LASTGIVEN and var.LASTGIVEN[nick] == victim: - pm(cli, nick, "You gave your totem to \u0002{0}\u0002 last time, you must choose someone else.".format(victim)) - return - type = "" - if role == "shaman": - type = " of " + var.TOTEMS[nick] - victim = choose_target(nick, victim) - if check_exchange(cli, nick, victim): - return - pm(cli, nick, ("You have given a totem{0} to \u0002{1}\u0002.").format(type, victim)) - totem = var.TOTEMS[nick] - if totem == "death": - if victim not in var.DYING: - var.DYING.append(victim) - elif totem == "protection": - if victim not in var.PROTECTED: - var.PROTECTED.append(victim) - elif totem == "revealing": - if victim not in var.REVEALED: - var.REVEALED.append(victim) - elif totem == "narcolepsy": - if victim not in var.ASLEEP: - var.ASLEEP.append(victim) - elif totem == "silence": - if victim not in var.TOBESILENCED: - var.TOBESILENCED.append(victim) - elif totem == "desperation": - if victim not in var.DESPERATE: - var.DESPERATE.append(victim) - elif totem == "impatience": # this totem stacks - var.IMPATIENT.append(victim) - elif totem == "pacifism": # this totem stacks - var.PACIFISTS.append(victim) - elif totem == "influence": - if victim not in var.INFLUENTIAL: - var.INFLUENTIAL.append(victim) - elif totem == "exchange": - if victim not in var.TOBEEXCHANGED: - var.TOBEEXCHANGED.append(victim) - elif totem == "lycanthropy": - if victim not in var.TOBELYCANTHROPES: - var.TOBELYCANTHROPES.append(victim) - elif totem == "luck": - if victim not in var.TOBELUCKY: - var.TOBELUCKY.append(victim) - elif totem == "pestilence": - if victim not in var.TOBEDISEASED: - var.TOBEDISEASED.append(victim) - elif totem == "retribution": - if victim not in var.RETRIBUTION: - var.RETRIBUTION.append(victim) - elif totem == "misdirection": - if victim not in var.TOBEMISDIRECTED: - var.TOBEMISDIRECTED.append(victim) + +def get_bitten_message(nick): + time_left = var.BITTEN[nick] + message = '' + if time_left <= 1: + message = ("You had the same dream again, but this time YOU were the pursuer. You smell fear from your quarry " + + "as you give an exhilerating chase, going only half your speed in order to draw out the fun. " + + "Suddenly your prey trips over a rock and falls down, allowing you to close in the remaining distance. " + + "You savor the fear in their eyes briefly before you raise your claw to deal a killing blow. " + + "Right before it connects, you wake up.") + elif time_left == 2: + message = ("You dreamt of running through the woods outside the village at night, wind blowing across your " + + "face as you weave between the pines. Suddenly you hear a rustling sound as a monstrous creature " + + "jumps out at you - a werewolf! You start running as fast as you can, you soon feel yourself falling " + + "down as you trip over a rock. You look up helplessly as the werewolf catches up to you, " + + "then wake up screaming.") else: - pm(cli, nick, "I don't know what to do with a '{0}' totem. This is a bug, please report it to the admins.".format(totem)) - var.LASTGIVEN[nick] = victim - var.SHAMANS.append(nick) - var.LOGGER.logBare(victim, "GIVEN TOTEM", nick) - chk_nightdone(cli) + message = ("You had a strange dream last night; a person was running away from something through a forest. " + + "They tripped and fell over a rock as a shadow descended upon them. Before you could actually see " + + "who or what the pursuer was, you woke with a start.") + return message + +@pmcmd("bite") +def bite_cmd(cli, nick, rest): + if var.PHASE in ("none", "join"): + cli.notice(nick, "No game is currently running.") + return + elif nick not in var.list_players() or nick in var.DISCONNECTED.keys(): + cli.notice(nick, "You're not currently playing.") + return + + role = var.get_role(nick) + if role != "alpha wolf": + pm(cli, nick, "Only an alpha wolf may use this command.") + return + if var.PHASE != "night": + pm(cli, nick, "You may only bite at night.") + return + if nick in var.ALPHA_WOLVES and nick not in var.BITE_PREFERENCES: + pm(cli, nick, "You have already bitten someone this game.") + return + if nick in var.SILENCED: + pm(cli, nick, "You have been silenced, and are unable to use any special powers.") + return + if not var.ALPHA_ENABLED: + pm(cli, nick, "You may only bite someone after another wolf has died during the day.") + return + + + pieces = [p.strip().lower() for p in re.split(" +",rest)] + victim = pieces[0] + + if var.ANGRY_WOLVES: + if not victim: + pm(cli, nick, "Please choose who to bite by specifying their nick.") + return + + pl = var.list_players() + pll = [x.lower() for x in pl] + + matches = 0 + for player in pll: + if victim == player: + target = player + break + if player.startswith(victim): + target = player + matches += 1 + else: + if matches != 1: + pm(cli, nick, "\u0002{0}\u0002 is currently not playing.".format(victim)) + return + victim = pl[pll.index(target)] + vrole = var.get_role(victim) + + if vrole in var.WOLFCHAT_ROLES: + pm(cli, nick, "You may not bite other wolves.") + return + + if nick not in var.ALPHA_WOLVES: + var.ALPHA_WOLVES.append(nick) + # this means that if victim is chosen by wolves, they will get bitten + # if victim isn't chosen, then a target is chosen at random from the victims + # (really only matters if wolves are angry; since there's only one target otherwise) + var.BITE_PREFERENCES[nick] = victim + + if victim: + pm(cli, nick, "You have chosen to bite \u0002{0}\u0002. If that player is not selected to be killed, you will bite one of the wolf targets at random instead.".format(victim)) + else: + pm(cli, nick, "You have chosen to bite tonight. Whomever the wolves select to be killed tonight will be bitten instead.") @pmcmd("pass") def pass_cmd(cli, nick, rest): @@ -4670,6 +4933,33 @@ def transition_night(cli): t2.daemon = True t2.start() + # convert bitten people to wolves, and advance bite stage + bittencopy = copy.copy(var.BITTEN) + for chump in bittencopy: + var.BITTEN[chump] -= 1 + # short-circuit if they are already a wolf + # this makes playing the day transition message easier since we can keep + # var.BITTEN around for a day after they turn + chumprole = var.get_role(chump) + + if chumprole in var.WOLF_ROLES: + del var.BITTEN[chump] + continue + + if var.BITTEN[chump] <= 0: + # now a wolf + pm(cli, chump, ("As you prepare for bed, you watch in horror as your body starts growing a coat of fur! " + + "Sudden realization hits you as you grin with your now muzzled face; that mysterious bite " + + "earlier slowly changed you into a werewolf! You feel bigger, stronger, faster, and ready to " + + "seize the night as you stealthily exit your home and search for the rest of your pack...")) + var.BITTEN_ROLES[chump] = chumprole + var.ROLES[chumprole].remove(chump) + var.ROLES["wolf"].append(chump) + var.FINAL_ROLES[chump] = "wolf" + for wolf in var.list_players(var.WOLFCHAT_ROLES): + if wolf != chump: + pm(cli, wolf, "\u0002{0}\u0002 is now a \u0002wolf\u0002!".format(chump)) + # convert amnesiac and kill village elder if necessary if var.NIGHT_COUNT == var.AMNESIAC_NIGHTS: amns = copy.copy(var.ROLES["amnesiac"]) @@ -4728,16 +5018,23 @@ def transition_night(cli): pm(cli, wolf, ('You are a \u0002wolf cub\u0002. While you cannot kill anyone, ' + 'the other wolves will become enraged if you die and will get ' + 'two kills the following night.')) + elif role == "alpha wolf": + pm(cli, wolf, ('You are an \u0002alpha wolf\u0002. Once per game following the death of another wolf ' + + 'during the day, you can choose to bite the wolves\' next target to turn ' + + 'them into a wolf instead of killing them. Kill villagers by using ' + '"kill " and "bite" to use your once-per-game bite power.')) else: # catchall in case we forgot something above - pm(cli, wolf, ('You are a \u0002{0}\u0002. There would normally be instructions ' + + an = 'n' if role[0] in ('a', 'e', 'i', 'o', 'u') else '' + pm(cli, wolf, ('You are a{0} \u0002{1}\u0002. There would normally be instructions ' + 'here, but someone forgot to add them in. Please report this to ' + - 'the admins, you can PM me "admins" for a list of available ones.').format(role)) + 'the admins, you can PM me "admins" for a list of available ones.').format(an, role)) if len(wolves) > 1: pm(cli, wolf, 'Also, if you PM me, your message will be relayed to other wolves.') else: - pm(cli, wolf, "You are a \02{0}{1}\02.".format(cursed, role)) # !simple + an = 'n' if cursed == '' and role[0] in ('a', 'e', 'i', 'o', 'u') else '' + pm(cli, wolf, "You are a{0} \02{1}{2}\02.".format(an, cursed, role)) # !simple pl = ps[:] random.shuffle(pl) @@ -4755,10 +5052,15 @@ def transition_night(cli): pm(cli, wolf, "Players: " + ", ".join(pl)) if wolf in var.WOLF_GUNNERS.keys() and var.WOLF_GUNNERS[wolf] > 0: pm(cli, wolf, "You have a \u0002gun\u0002 with {0} bullet{1}.".format(var.WOLF_GUNNERS[wolf], "s" if var.WOLF_GUNNERS[wolf] > 1 else "")) + angry_alpha = '' if var.DISEASED_WOLVES: pm(cli, wolf, 'You are feeling ill tonight, and are unable to kill anyone.') - elif var.ANGRY_WOLVES and role in ("wolf", "werecrow"): + elif var.ANGRY_WOLVES and role in ("wolf", "werecrow", "alpha wolf"): pm(cli, wolf, 'You are \u0002angry\u0002 tonight, and may kill two targets by using "kill and ".') + angry_alpha = ' ' + if var.ALPHA_ENABLED and role == "alpha wolf" and wolf not in var.ALPHA_WOLVES: + pm(cli, wolf, ('You may use "bite{0}" tonight in order to turn the wolves\' target into a wolf instead of killing them. ' + + 'They will turn into a wolf in {1} night{2}.').format(angry_alpha, var.ALPHA_WOLF_NIGHTS, 's' if var.ALPHA_WOLF_NIGHTS > 1 else '')) for seer in var.list_players(["seer", "oracle", "augur"]): pl = ps[:] @@ -4938,6 +5240,19 @@ def transition_night(cli): pm(cli, hunter, "You are a \u0002hunter\u0002.") pm(cli, hunter, "Players: " + ", ".join(pl)) + for doctor in var.ROLES["doctor"]: + if var.DOCTORS[doctor] > 0: # has immunizations remaining + pl = ps[:] + random.shuffle(pl) + if doctor in var.PLAYERS and not is_user_simple(doctor): + pm(cli, doctor, ('You are a \u0002doctor\u0002. You can give out immunizations to ' + + 'villagers by using "give " in PM during the daytime. ' + + 'An immunized villager will die instead of turning into a wolf due to the ' + + 'alpha wolf\'s or lycan\'s power.')) + else: + pm(cli, doctor, "You are a \u0002doctor\u0002.") + pm(cli, doctor, 'You have \u0002{0}\u0002 immunization{1}.'.format(var.DOCTORS[doctor], 's' if var.DOCTORS[doctor] > 1 else '')) + for fool in var.ROLES["fool"]: if fool in var.PLAYERS and not is_user_simple(fool): pm(cli, fool, ('You are a \u0002fool\u0002. The game immediately ends with you ' + @@ -5107,7 +5422,7 @@ def transition_night(cli): var.LOGGER.logBare("NIGHT", "BEGIN") # cli.msg(chan, "DEBUG: "+str(var.ROLES)) - if len(var.ROLES["wolf"] + var.ROLES["werecrow"]) == 0 or var.DISEASED_WOLVES: # Probably something interesting going on. + if len(var.list_players(var.WOLF_ROLES)) - len(var.ROLES["wolf cub"]) == 0 or var.DISEASED_WOLVES: # Probably something interesting going on. chk_nightdone(cli) chk_traitor(cli) @@ -5286,6 +5601,14 @@ def start(cli, nick, chan, forced = False): var.OTHER_KILLS = {} var.ACTED_EXTRA = 0 var.ABSTAINED = False + var.DOCTORS = {} + var.IMMUNIZED = set() + var.CURED_LYCANS = [] + var.ALPHA_WOLVES = [] + var.ALPHA_ENABLED = False + var.BITTEN = {} + var.BITE_PREFERENCES = {} + var.BITTEN_ROLES = {} for role, count in addroles.items(): if role in var.TEMPLATE_RESTRICTIONS.keys(): @@ -5363,6 +5686,10 @@ def start(cli, nick, chan, forced = False): for amnesiac in var.ROLES["amnesiac"]: var.FINAL_ROLES[amnesiac] = random.choice(amnroles) + # Handle doctor + for doctor in var.ROLES["doctor"]: + var.DOCTORS[doctor] = math.ceil(var.DOCTOR_IMMUNIZATION_MULTIPLIER * len(pl)) + var.DAY_TIMEDELTA = timedelta(0) var.NIGHT_TIMEDELTA = timedelta(0) var.DAY_START_TIME = datetime.now() diff --git a/settings/wolfgame.py b/settings/wolfgame.py index 72f50b0..efe8190 100644 --- a/settings/wolfgame.py +++ b/settings/wolfgame.py @@ -90,6 +90,9 @@ DETECTIVE_REVEALED_CHANCE = 2/5 SHARPSHOOTER_CHANCE = 1/5 # if sharpshooter is enabled, chance that a gunner will become a sharpshooter instead AMNESIAC_NIGHTS = 3 # amnesiac gets to know their actual role on this night +ALPHA_WOLF_NIGHTS = 3 # alpha wolf turns the target into a wolf after this many nights (note the night they are bitten is considered night 1) + +DOCTOR_IMMUNIZATION_MULTIPLIER = 0.17 # ceil(num_players * multiplier) = number of immunizations # SHAMAN CRAZED SHAMAN TOTEM_CHANCES = { "death": ( 1/8 , 1/15 ), @@ -141,6 +144,7 @@ ROLE_GUIDE = {# village roles "mad scientist" : ( 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ), "hunter" : ( 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ), "shaman" : ( 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ), + "doctor" : ( 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ), # wolf roles "wolf" : ( 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 2 , 2 , 2 , 2 , 3 , 3 , 3 ), "traitor" : ( 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ), @@ -150,6 +154,7 @@ ROLE_GUIDE = {# village roles "hag" : ( 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 ), "wolf cub" : ( 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ), "sorcerer" : ( 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 ), + "alpha wolf" : ( 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ), # neutral roles "lycan" : ( 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ), "vengeful ghost" : ( 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ), @@ -171,23 +176,23 @@ ROLE_GUIDE = {# village roles # Harlot dies when visiting, gunner kills when shooting, GA and bodyguard have a chance at dying when guarding # If every wolf role dies, the game ends and village wins and there are no remaining traitors, the game ends and villagers win -WOLF_ROLES = ["wolf", "werecrow", "wolf cub"] +WOLF_ROLES = ["wolf", "alpha wolf", "werecrow", "wolf cub"] # Access to wolfchat, and counted towards the # of wolves vs villagers when determining if a side has won -WOLFCHAT_ROLES = ["wolf", "werecrow", "wolf cub", "traitor", "hag", "sorcerer"] +WOLFCHAT_ROLES = ["wolf", "alpha wolf", "werecrow", "wolf cub", "traitor", "hag", "sorcerer"] # Wins with the wolves, even if the roles are not necessarily wolves themselves -WOLFTEAM_ROLES = ["wolf", "werecrow", "wolf cub", "traitor", "hag", "sorcerer", "minion", "cultist"] +WOLFTEAM_ROLES = ["wolf", "alpha wolf", "werecrow", "wolf cub", "traitor", "hag", "sorcerer", "minion", "cultist"] # These roles never win as a team, only ever individually (either instead of or in addition to the regular winners) TRUE_NEUTRAL_ROLES = ["crazed shaman", "fool", "jester", "monster", "clone"] # These are the roles that will NOT be used for when amnesiac turns, everything else is fair game! -AMNESIAC_BLACKLIST = ["monster", "amnesiac", "minion", "matchmaker", "clone", "villager", "cultist"] +AMNESIAC_BLACKLIST = ["monster", "amnesiac", "minion", "matchmaker", "clone", "doctor", "villager", "cultist"] # 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 list. Templates not listed here are considered full roles instead -TEMPLATE_RESTRICTIONS = {"cursed villager" : ["wolf", "wolf cub", "werecrow", "seer", "oracle", "augur", "fool", "jester", "mad scientist"], - "gunner" : ["wolf", "traitor", "werecrow", "hag", "wolf cub", "sorcerer", "minion", "cultist", "fool", "lycan", "jester"], - "sharpshooter" : ["wolf", "traitor", "werecrow", "hag", "wolf cub", "sorcerer", "minion", "cultist", "fool", "lycan", "jester"], +TEMPLATE_RESTRICTIONS = {"cursed villager" : WOLF_ROLES + ["seer", "oracle", "augur", "fool", "jester", "mad scientist"], + "gunner" : WOLFTEAM_ROLES + ["fool", "lycan", "jester"], + "sharpshooter" : WOLFTEAM_ROLES + ["fool", "lycan", "jester"], "mayor" : ["fool", "jester", "monster"], - "assassin" : ["seer", "augur", "oracle", "harlot", "detective", "bodyguard", "guardian angel", "village drunk", "hunter", "shaman", "crazed shaman", "fool", "mayor", "wolf", "werecrow", "wolf cub", "traitor", "lycan"], + "assassin" : WOLF_ROLES + ["traitor", "seer", "augur", "oracle", "harlot", "detective", "bodyguard", "guardian angel", "village drunk", "hunter", "shaman", "crazed shaman", "fool", "mayor", "lycan", "doctor"], "bureaucrat" : [], } @@ -299,6 +304,10 @@ def del_player(pname): tpls = get_templates(pname) for t in tpls: ROLES[t].remove(pname) + if pname in BITTEN: + del BITTEN[pname] + if pname in BITTEN_ROLES: + del BITTEN_ROLES[pname] def get_templates(nick): tpl = []