From 116653432b0bf3e88d9c4b8dc0e0866f07f0b023 Mon Sep 17 00:00:00 2001 From: skizzerz Date: Mon, 13 Jul 2015 02:35:14 -0500 Subject: [PATCH] !stats Overhaul Decoupled ROLE_REVEAL from impacting !stats, added new STATS_TYPE. ROLE_REVEAL is now a string: - on = roles revealed on death - off = roles not revealed on death - team = user's team revealed on death but not their role STATS_TYPE is also a string: - default = stats are calculated as if a villager or nonplayer was manually tracking who was what; this leads to !stats not revealing any information that is not publicly known - accurate = what it used to be with a couple modifications; all roles are shown (VG and time lord are no longer lumped with villager), alpha wolf and lycans turning are hidden, nothing else is hidden (so you can see what amnesiac turned into or when clone turns) - team = only shows number of people on each team, calculated the same way as accurate - disabled = stats are disabled, doing !stats only serves as a mass-ping of alive players Removed random_reveal/random_noreveal hack - !fgame random=role reveal:on|off does the same thing as the hack used to do. Allow all game modes to take arguments (aka the blah in roles=blah). - Allowed arguments are "role reveal" (on/off/team), "stats type" (default/accurate/team/disabled), and "abstain" (enabled/restricted/disabled). - roles obviously still allows for taking individual roles as arguments as well as specifying a default role, however this functionality is not available for any other gamemode. - if any custom options are set as a result of this or changing them in game mode constructors, players will be informed of which options are in play. Performing !frole will force STATS_TYPE from default to accurate, this is done because the default logic is based not on what roles are currently in existence, but rather what roles the game was started with and what transformations could have applied to them. !frole throws a complete monkey wrench in that to the point where it is impossible to nicely recover. Clone and Traitor are now unconditionally blacklisted for amnesiac, as the additional logic to support them in default !stats is incredibly complex and as such was not done for this PR. Matchmaker is conditionally blacklisted for amnesiac if amnesiacs turn after the first night. It is an allowed choice (unless it appears on the config blacklist) if amnesiacs turn night 1. --- src/settings.py | 127 +++++--- src/wolfgame.py | 799 ++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 733 insertions(+), 193 deletions(-) diff --git a/src/settings.py b/src/settings.py index d256a67..5175105 100644 --- a/src/settings.py +++ b/src/settings.py @@ -76,7 +76,8 @@ HIDDEN_CLONE = False GUARDIAN_ANGEL_CAN_GUARD_SELF = True START_WITH_DAY = False WOLF_STEALS_GUN = True # at night, the wolf can steal steal the victim's bullets -ROLE_REVEAL = True +ROLE_REVEAL = "on" # on/off/team - what role information is shown on death +STATS_TYPE = "default" # default/accurate/team/disabled - what role information is shown when doing !stats LOVER_WINS_WITH_FOOL = False # if fool is lynched, does their lover win with them? DEFAULT_SEEN_AS_VILL = True # non-wolves are seen as villager regardless of the default role @@ -355,13 +356,23 @@ def get_role(p): def get_reveal_role(nick): if HIDDEN_TRAITOR and get_role(nick) == "traitor": - return DEFAULT_ROLE + role = DEFAULT_ROLE elif HIDDEN_AMNESIAC and nick in ORIGINAL_ROLES["amnesiac"]: - return "amnesiac" + role = "amnesiac" elif HIDDEN_CLONE and nick in ORIGINAL_ROLES["clone"]: - return "clone" + role = "clone" else: - return get_role(nick) + role = get_role(nick) + + if ROLE_REVEAL != "team": + return role + + if role in WOLFTEAM_ROLES: + return "wolf" + elif role in TRUE_NEUTRAL_ROLES: + return "neutral player" + else: + return "villager" def del_player(pname): prole = get_role(pname) @@ -427,6 +438,43 @@ def reset_roles(index): # TODO: move this to src/gamemodes.py class GameMode: + def __init__(self, arg=""): + if not arg: + return + + pairs = arg.split(",") + for pair in pairs: + change = pair.lower().split(":") + if len(change) != 2: + raise InvalidModeException("Invalid syntax for mode arguments. arg={0}".format(arg)) + + key, val = change + if key in ("role reveal", "reveal roles"): + if val not in ("on", "off", "team"): + raise InvalidModeException(("Did not recognize value \u0002{0}\u0002 for role reveal. "+ + "Allowed values: on, off, team").format(val)) + self.ROLE_REVEAL = val + if val == "off" and not hasattr(self, "STATS_TYPE"): + self.STATS_TYPE = "disabled" + elif val == "team" and not hasattr(self, "STATS_TYPE"): + self.STATS_TYPE = "team" + elif key in ("stats type", "stats"): + if val not in ("default", "accurate", "team", "disabled"): + raise InvalidModeException(("Did not recognize value \u0002{0}\u0002 for stats type. "+ + "Allowed values: default, accurate, team, disabled").format(val)) + self.STATS_TYPE = val + elif key == "abstain": + if val not in ("enabled", "restricted", "disabled"): + raise InvalidModeException(("Did not recognize value \u0002{0}\u0002 for abstain. "+ + "Allowed values: enabled, restricted, disabled").format(val)) + if val == "enabled": + self.ABSTAIN_ENABLED = True + self.LIMIT_ABSTAIN = False + elif val == "restricted": + self.ABSTAIN_ENABLED = True + self.LIMIT_ABSTAIN = True + elif val == "disabled": + self.ABSTAIN_ENABLED = False def startup(self): pass @@ -437,7 +485,8 @@ class GameMode: class ChangedRolesMode(GameMode): """Example: !fgame roles=wolf:1,seer:0,guardian angel:1""" - def __init__(self, arg = ""): + def __init__(self, arg=""): + super().__init__(arg) self.MAX_PLAYERS = 35 self.ROLE_GUIDE = ROLE_GUIDE.copy() self.ROLE_INDEX = (MIN_PLAYERS,) @@ -459,16 +508,9 @@ class ChangedRolesMode(GameMode): self.ROLE_GUIDE[role.lower()] = tuple([int(num)] * len(ROLE_INDEX)) elif role.lower() == "default" and num.lower() in self.ROLE_GUIDE: self.DEFAULT_ROLE = num.lower() - elif role.lower() == "role reveal" or role.lower() == "reveal roles": - num = num.lower() - if num in ("on", "true", "yes", "1"): - self.ROLE_REVEAL = True - elif num in ("off", "false", "no", "0"): - self.ROLE_REVEAL = False - elif num == "partial": - self.ROLE_REVEAL = "partial" - else: - raise InvalidModeException("Did not recognize value \u0002{0}\u0002 for role reveal.".format(num)) + elif role.lower() in ("role reveal", "reveal roles", "stats type", "stats", "abstain"): + # handled in parent constructor + pass else: raise InvalidModeException(("The role \u0002{0}\u0002 "+ "is not valid.").format(role)) @@ -478,14 +520,15 @@ class ChangedRolesMode(GameMode): @game_mode("default", minp = 4, maxp = 24, likelihood = 20) class DefaultMode(GameMode): """Default game mode.""" - def __init__(self): + def __init__(self, arg=""): # No extra settings, just an explicit way to revert to default settings - pass + super().__init__(arg) @game_mode("foolish", minp = 8, maxp = 24, likelihood = 8) class FoolishMode(GameMode): """Contains the fool, be careful not to lynch them!""" - def __init__(self): + def __init__(self, arg=""): + super().__init__(arg) self.ROLE_INDEX = ( 8 , 9 , 10 , 11 , 12 , 15 , 17 , 20 , 21 , 22 , 24 ) self.ROLE_GUIDE = reset_roles(self.ROLE_INDEX) self.ROLE_GUIDE.update({# village roles @@ -513,7 +556,8 @@ class FoolishMode(GameMode): @game_mode("mad", minp = 7, maxp = 22, likelihood = 8) class MadMode(GameMode): """This game mode has mad scientist and many things that may kill you.""" - def __init__(self): + def __init__(self, arg=""): + super().__init__(arg) # gunner and sharpshooter always get 1 bullet self.SHOTS_MULTIPLIER = 0.0001 self.SHARPSHOOTER_MULTIPLIER = 0.0001 @@ -546,10 +590,11 @@ class MadMode(GameMode): @game_mode("evilvillage", minp = 6, maxp = 18, likelihood = 1) class EvilVillageMode(GameMode): """Majority of the village is wolf aligned, safes must secretly try to kill the wolves.""" - def __init__(self): + def __init__(self, arg=""): + self.ABSTAIN_ENABLED = False + super().__init__(arg) self.DEFAULT_ROLE = "cultist" self.DEFAULT_SEEN_AS_VILL = False - self.ABSTAIN_ENABLED = False self.ROLE_INDEX = ( 6 , 8 , 10 , 12 , 15 ) self.ROLE_GUIDE = reset_roles(self.ROLE_INDEX) self.ROLE_GUIDE.update({# village roles @@ -614,7 +659,8 @@ class EvilVillageMode(GameMode): @game_mode("classic", minp = 7, maxp = 21, likelihood = 4) class ClassicMode(GameMode): """Classic game mode from before all the changes.""" - def __init__(self): + def __init__(self, arg=""): + super().__init__(arg) self.ABSTAIN_ENABLED = False self.ROLE_INDEX = ( 4 , 6 , 8 , 10 , 12 , 15 , 17 , 18 , 20 ) self.ROLE_GUIDE = reset_roles(self.ROLE_INDEX) @@ -636,7 +682,8 @@ class ClassicMode(GameMode): @game_mode("rapidfire", minp = 6, maxp = 24, likelihood = 0) class RapidFireMode(GameMode): """Many roles that lead to multiple chain deaths.""" - def __init__(self): + def __init__(self, arg=""): + super().__init__(arg) self.SHARPSHOOTER_CHANCE = 1 self.DAY_TIME_LIMIT = 480 self.DAY_TIME_WARN = 360 @@ -668,7 +715,8 @@ class RapidFireMode(GameMode): @game_mode("drunkfire", minp = 8, maxp = 17, likelihood = 0) class DrunkFireMode(GameMode): """Most players get a gun, quickly shoot all the wolves!""" - def __init__(self): + def __init__(self, arg=""): + super().__init__(arg) self.SHARPSHOOTER_CHANCE = 1 self.DAY_TIME_LIMIT = 480 self.DAY_TIME_WARN = 360 @@ -699,8 +747,9 @@ class DrunkFireMode(GameMode): @game_mode("noreveal", minp = 4, maxp = 21, likelihood = 2) class NoRevealMode(GameMode): """Roles are not revealed when players die.""" - def __init__(self): - self.ROLE_REVEAL = False + def __init__(self, arg=""): + self.ROLE_REVEAL = "off" + super().__init__(arg) self.ROLE_INDEX = ( 4 , 6 , 8 , 10 , 12 , 15 , 17 , 19 ) self.ROLE_GUIDE = reset_roles(self.ROLE_INDEX) self.ROLE_GUIDE.update({# village roles @@ -725,7 +774,8 @@ class NoRevealMode(GameMode): @game_mode("lycan", minp = 7, maxp = 21, likelihood = 6) class LycanMode(GameMode): """Many lycans will turn into wolves. Hunt them down before the wolves overpower the village.""" - def __init__(self): + def __init__(self, arg=""): + super().__init__(arg) self.ROLE_INDEX = ( 7 , 8 , 9 , 10 , 11 , 12 , 15 , 17 , 20 ) self.ROLE_GUIDE = reset_roles(self.ROLE_INDEX) self.ROLE_GUIDE.update({# village roles @@ -750,7 +800,8 @@ class LycanMode(GameMode): @game_mode("valentines", minp = 8, maxp = 24, likelihood = 0) class MatchmakerMode(GameMode): """Love is in the air!""" - def __init__(self): + def __init__(self, arg=""): + super().__init__(arg) self.ROLE_INDEX = range(8, 25) self.ROLE_GUIDE = reset_roles(self.ROLE_INDEX) self.ROLE_GUIDE.update({ @@ -763,11 +814,13 @@ class MatchmakerMode(GameMode): @game_mode("random", minp = 8, maxp = 24, likelihood = 0, conceal_roles = True) class RandomMode(GameMode): """Completely random and hidden roles.""" - def __init__(self): + def __init__(self, arg=""): + self.ROLE_REVEAL = random.choice(("on", "off", "team")) + self.STATS_TYPE = "disabled" + super().__init__(arg) self.LOVER_WINS_WITH_FOOL = True self.MAD_SCIENTIST_SKIPS_DEAD_PLAYERS = 0 # always make it happen self.ALPHA_WOLF_NIGHTS = 2 - self.ROLE_REVEAL = random.choice(("partial", False)) self.TEMPLATE_RESTRICTIONS = {template: [] for template in TEMPLATE_RESTRICTIONS} self.TOTEM_CHANCES = { # shaman , crazed @@ -818,7 +871,8 @@ class RandomMode(GameMode): @game_mode("aleatoire", minp = 8, maxp = 24, likelihood = 4) class AleatoireMode(GameMode): """Game mode created by Metacity and balanced by woffle.""" - def __init__(self): + def __init__(self, arg=""): + super().__init__(arg) self.SHARPSHOOTER_CHANCE = 1 # SHAMAN , CRAZED SHAMAN self.TOTEM_CHANCES = { "death": ( 4 , 1 ), @@ -869,7 +923,8 @@ class AleatoireMode(GameMode): @game_mode("alpha", minp = 7, maxp = 24, likelihood = 5) class AlphaMode(GameMode): """Features the alpha wolf who can turn other people into wolves, be careful whom you trust!""" - def __init__(self): + def __init__(self, arg=""): + super().__init__(arg) self.ROLE_INDEX = ( 7 , 8 , 10 , 11 , 12 , 14 , 15 , 17 , 18 , 20 , 21 , 24 ) self.ROLE_GUIDE = reset_roles(self.ROLE_INDEX) self.ROLE_GUIDE.update({ @@ -897,8 +952,9 @@ class AlphaMode(GameMode): @game_mode("guardian", minp = 8, maxp = 16, likelihood = 0) class GuardianMode(GameMode): """Game mode full of guardian angels, wolves need to pick them apart!""" - def __init__(self): + def __init__(self, arg=""): self.LIMIT_ABSTAIN = False + super().__init__(arg) self.ROLE_INDEX = ( 8 , 10 , 12 , 13 , 15 ) self.ROLE_GUIDE = reset_roles(self.ROLE_INDEX) self.ROLE_GUIDE.update({ @@ -978,7 +1034,8 @@ class GuardianMode(GameMode): @game_mode("charming", minp = 5, maxp = 24, likelihood = 4) class CharmingMode(GameMode): """Charmed players must band together to find the piper in this game mode.""" - def __init__(self): + def __init__(self, arg=""): + super().__init__(arg) self.ROLE_INDEX = ( 5 , 6 , 8 , 10 , 11 , 12 , 14 , 16 , 18 , 19 , 22 , 24 ) self.ROLE_GUIDE = reset_roles(self.ROLE_INDEX) self.ROLE_GUIDE.update({# village roles diff --git a/src/wolfgame.py b/src/wolfgame.py index 2263c56..f75361e 100644 --- a/src/wolfgame.py +++ b/src/wolfgame.py @@ -102,6 +102,7 @@ var.OPPED = False # Keeps track of whether the bot is opped var.BITTEN = {} var.BITTEN_ROLES = {} +var.LYCAN_ROLES = {} var.VENGEFUL_GHOSTS = {} var.CHARMED = set() @@ -1251,96 +1252,501 @@ def stats(cli, nick, chan, rest): else: cli.notice(nick, msg) - if var.PHASE == "join" or var.ROLE_REVEAL is not True: + if var.PHASE == "join" or var.STATS_TYPE == "disabled": return message = [] - l1 = [k for k in var.ROLES.keys() - if var.ROLES[k]] - l2 = [k for k in var.ORIGINAL_ROLES.keys() - if var.ORIGINAL_ROLES[k]] - rs = set(l1+l2) - rs = [role for role in var.role_order() if role in rs] - # picky ordering: villager always last - if var.DEFAULT_ROLE in rs: - rs.remove(var.DEFAULT_ROLE) - rs.append(var.DEFAULT_ROLE) - - - amn_roles = defaultdict(int) - for amn in var.ORIGINAL_ROLES["amnesiac"]: - if amn not in pl: - continue - - amnrole = var.get_role(amn) - if amnrole == "time lord": - amnrole = "villager" - elif amnrole == "vengeful ghost": - amnrole = var.DEFAULT_ROLE - elif amnrole == "traitor" and var.HIDDEN_TRAITOR: - amnrole = var.DEFAULT_ROLE - if amnrole != "amnesiac": - amn_roles["amnesiac"] += 1 - amn_roles[amnrole] -= 1 - - bitten_roles = defaultdict(int) - for role in var.BITTEN_ROLES.values(): - bitten_roles[role] += 1 - - vb = "are" - for role in rs: - # only show actual roles - if role in ("time lord", "vengeful ghost") or role in var.TEMPLATE_RESTRICTIONS.keys(): - continue - 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"] - elif role == var.DEFAULT_ROLE: - if var.HIDDEN_TRAITOR: - count += len(var.ROLES["traitor"]) - count += bitten_roles["traitor"] - if var.DEFAULT_ROLE == "villager": - count += len(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["time lord"] - count += bitten_roles["vengeful ghost"] - else: - count += len(var.ROLES["vengeful ghost"]) - count += bitten_roles["vengeful ghost"] - count += bitten_roles[var.DEFAULT_ROLE] - elif role == "villager": - count += len(var.ROLES["time lord"]) - count -= len([p for p in var.CURED_LYCANS if p in var.ROLES["villager"]]) - count += bitten_roles["villager"] - count += bitten_roles["time lord"] - elif role == "wolf": - count -= sum(bitten_roles.values()) - # GAs turn into FAs, not wolves - count += bitten_roles["guardian angel"] - elif role == "fallen angel": - count -= bitten_roles["guardian angel"] - else: - count += bitten_roles[role] - - 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: + # Instead of looping over the current roles, we start with the original set and apply + # changes to it as public game events occur. This way, !stats output should duplicate + # what a player would have if they were manually tracking who is what and did not + # have any non-public information. The comments below explain the logic such a player + # would be using to derive the list. Note that this logic is based on the assumption + # that role reveal is on. If role reveal is off or team, stats type should probably be + # set to disabled or team respectively instead of this, as this will then leak info. + if var.STATS_TYPE == "default": + # role: [min, max] -- "we may not necessarily know *exactly* how + # many of a particular role there are, but we know that there is + # between min and max of them" + rolecounts = defaultdict(lambda: [0, 0]) + start_roles = set() + orig_roles = {} + equiv_sets = {} + total_immunizations = 0 + extra_lycans = 0 + # Step 1. Get our starting set of roles. This also calculates the maximum numbers for equivalency sets + # (sets of roles that are decremented together because we can't know for sure which actually died). + for r, v in var.ORIGINAL_ROLES.items(): + if r in var.TEMPLATE_RESTRICTIONS.keys(): continue - message.append("\u0002{0}\u0002 {1}".format(count if count else "\u0002no\u0002", var.plural(role))) + if len(v) == 0: + continue + start_roles.add(r) + rolecounts[r] = [len(v), len(v)] + for p in v: + orig_roles[p] = r + + total_immunizations = rolecounts["doctor"][0] * math.ceil(len(var.ALL_PLAYERS) * var.DOCTOR_IMMUNIZATION_MULTIPLIER) + if "amnesiac" in start_roles and "doctor" not in var.AMNESIAC_BLACKLIST: + total_immunizations += rolecounts["amnesiac"][0] * math.ceil(len(var.ALL_PLAYERS) * var.DOCTOR_IMMUNIZATION_MULTIPLIER) + + extra_lycans = rolecounts["lycan"][0] - min(total_immunizations, rolecounts["lycan"][0]) + + equiv_sets["traitor_default"] = rolecounts["traitor"][0] + rolecounts[var.DEFAULT_ROLE][0] + equiv_sets["lycan_villager"] = min(rolecounts["lycan"][0], total_immunizations) + rolecounts["villager"][0] + equiv_sets["traitor_lycan_villager"] = equiv_sets["traitor_default"] + equiv_sets["lycan_villager"] - rolecounts[var.DEFAULT_ROLE][0] + equiv_sets["amnesiac_clone"] = rolecounts["amnesiac"][0] + rolecounts["clone"][0] + equiv_sets["amnesiac_clone_cub"] = rolecounts["amnesiac"][0] + rolecounts["clone"][0] + rolecounts["wolf cub"][0] + equiv_sets["wolf_fallen"] = 0 + equiv_sets["fallen_guardian"] = 0 + if var.TRAITOR_TURNED: + equiv_sets["traitor_default"] -= rolecounts["traitor"][0] + equiv_sets["traitor_lycan_villager"] -= rolecounts["traitor"][0] + rolecounts["wolf"][0] += rolecounts["traitor"][0] + rolecounts["wolf"][1] += rolecounts["traitor"][1] + rolecounts["traitor"] = [0, 0] + # Step 2. Handle role swaps via exchange totem by modifying orig_roles -- the original + # roles themselves didn't change, just who has them. By doing the swap early on we greatly + # simplify the death logic below in step 3 -- to an outsider that doesn't know any info + # the role swap might as well never happened and those people simply started with those roles; + # they can't really tell the difference. + for a, b in var.EXCHANGED_ROLES: + orig_roles[a], orig_roles[b] = orig_roles[b], orig_roles[a] + # Step 3. Work out people that turned into wolves via either alpha wolf, lycan, or lycanthropy totem + # All three of those play the same "chilling howl" message, once per additional wolf + num_alpha = rolecounts["alpha wolf"][0] + num_angel = rolecounts["guardian angel"][0] + if "amnesiac" in start_roles and "guardian angel" not in var.AMNESIAC_BLACKLIST: + num_angel += rolecounts["amnesiac"][0] + have_lycan_totem = False + for idx, shaman in enumerate(var.TOTEM_ORDER): + if (shaman in start_roles or ("amnesiac" in start_roles and shaman not in var.AMNESIAC_BLACKLIST)) and var.TOTEM_CHANCES["lycanthropy"][idx] > 0: + have_lycan_totem = True + + extra_wolves = var.EXTRA_WOLVES + num_wolves = rolecounts["wolf"][0] + num_fallen = rolecounts["fallen angel"][0] + while extra_wolves > 0: + extra_wolves -= 1 + if num_alpha == 0 and not have_lycan_totem: + # This is easy, all of our extra wolves are actual lycans, and we know this for a fact + rolecounts["wolf"][0] += 1 + rolecounts["wolf"][1] += 1 + num_wolves += 1 + + if rolecounts["lycan"][1] > 0: + rolecounts["lycan"][0] -= 1 + rolecounts["lycan"][1] -= 1 + else: + # amnesiac or clone became lycan and was subsequently turned + maxcount = max(0, equiv_sets["amnesiac_clone"] - 1) + + rolecounts["amnesiac"][0] = max(0, rolecounts["amnesiac"][0] - 1) + if rolecounts["amnesiac"][1] > maxcount: + rolecounts["amnesiac"][1] = maxcount + + rolecounts["clone"][0] = max(0, rolecounts["clone"][0] - 1) + if rolecounts["clone"][1] > maxcount: + rolecounts["clone"][1] = maxcount + + equiv_sets["amnesiac_clone"] = maxcount + + + if extra_lycans > 0: + extra_lycans -= 1 + else: + equiv_sets["lycan_villager"] = max(0, equiv_sets["lycan_villager"] - 1) + equiv_sets["traitor_lycan_villager"] = max(0, equiv_sets["traitor_lycan_villager"] - 1) + elif num_alpha == 0 or num_angel == 0: + # We are guaranteed to have gotten an additional wolf, but we can't guarantee it was an actual lycan + rolecounts["wolf"][0] += 1 + rolecounts["wolf"][1] += 1 + num_wolves += 1 + rolecounts["lycan"][0] = max(0, rolecounts["lycan"][0] - 1) + + # apply alphas before lycan totems (in case we don't actually have lycan totems) + # this way if we don't have totems and alphas is 0 we hit guaranteed lycans above + if num_alpha > 0: + num_alpha -= 1 + else: + # We may have gotten an additional wolf or an additional fallen angel, we don't necessarily know which + num_alpha -= 1 + num_angel -= 1 + rolecounts["wolf"][1] += 1 + rolecounts["fallen angel"][1] += 1 + rolecounts["guardian angel"][0] -= 1 + equiv_sets["wolf_fallen"] += 1 + equiv_sets["fallen_guardian"] += 1 + + # Step 4. Remove all dead players + # When rolesets are a thing (e.g. one of x, y, or z), those will be resolved here as well + for p in var.ALL_PLAYERS: + if p in pl: + continue + # pr should be the role the person gets revealed as should they die + pr = orig_roles[p] + if p in var.FINAL_ROLES and pr not in ("amnesiac", "clone"): + pr = var.FINAL_ROLES[p] + elif pr == "amnesiac" and not var.HIDDEN_AMNESIAC and p in var.FINAL_ROLES: + pr = var.FINAL_ROLES[p] + elif pr == "clone" and not var.HIDDEN_CLONE and p in var.FINAL_ROLES: + pr = var.FINAL_ROLES[p] + elif pr == "traitor" and var.TRAITOR_TURNED: + # we turned every traitor into wolf above, which means even though + # this person died as traitor, we need to deduct the count from wolves + pr = "wolf" + elif pr == "traitor" and var.HIDDEN_TRAITOR: + pr = var.DEFAULT_ROLE + + # set to true if we kill more people than exist in a given role, + # which means that amnesiac or clone must have became that role + overkill = False + + if pr == var.DEFAULT_ROLE: + # the person that died could have been traitor or an immunized lycan + if var.DEFAULT_ROLE == "villager": + maxcount = equiv_sets["traitor_lycan_villager"] + else: + maxcount = equiv_sets["traitor_default"] + + if maxcount == 0: + overkill = True + + maxcount = max(0, maxcount - 1) + if var.HIDDEN_TRAITOR and not var.TRAITOR_TURNED: + rolecounts["traitor"][0] = max(0, rolecounts["traitor"][0] - 1) + if rolecounts["traitor"][1] > maxcount: + rolecounts["traitor"][1] = maxcount + + if var.DEFAULT_ROLE == "villager" and total_immunizations > 0: + total_immunizations -= 1 + rolecounts["lycan"][0] = max(0, rolecounts["lycan"][0] - 1) + if rolecounts["lycan"][1] > maxcount + extra_lycans: + rolecounts["lycan"][1] = maxcount + extra_lycans + + rolecounts[pr][0] = max(0, rolecounts[pr][0] - 1) + if rolecounts[pr][1] > maxcount: + rolecounts[pr][1] = maxcount + + if var.DEFAULT_ROLE == "villager": + equiv_sets["traitor_lycan_villager"] = maxcount + else: + equiv_sets["traitor_default"] = maxcount + elif pr == "villager": + # the villager that died could have been an immunized lycan + maxcount = max(0, equiv_sets["lycan_villager"] - 1) + + if equiv_sets["lycan_villager"] == 0: + overkill = True + + if total_immunizations > 0: + total_immunizations -= 1 + rolecounts["lycan"][0] = max(0, rolecounts["lycan"][0] - 1) + if rolecounts["lycan"][1] > maxcount + extra_lycans: + rolecounts["lycan"][1] = maxcount + extra_lycans + + rolecounts[pr][0] = max(0, rolecounts[pr][0] - 1) + if rolecounts[pr][1] > maxcount: + rolecounts[pr][1] = maxcount + + equiv_sets["lycan_villager"] = maxcount + elif pr == "lycan": + # non-immunized lycan, reduce counts appropriately + if rolecounts[pr][1] == 0: + overkill = True + rolecounts[pr][0] = max(0, rolecounts[pr][0] - 1) + rolecounts[pr][1] = max(0, rolecounts[pr][1] - 1) + + if extra_lycans > 0: + extra_lycans -= 1 + else: + equiv_sets["lycan_villager"] = max(0, equiv_sets["lycan_villager"] - 1) + equiv_sets["traitor_lycan_villager"] = max(0, equiv_sets["traitor_lycan_villager"] - 1) + elif pr == "wolf": + # person that died could have possibly been turned by alpha + if rolecounts[pr][1] == 0: + # this overkill either means that we're hitting amnesiac/clone or that cubs turned + overkill = True + rolecounts[pr][0] = max(0, rolecounts[pr][0] - 1) + rolecounts[pr][1] = max(0, rolecounts[pr][1] - 1) + + if num_wolves > 0: + num_wolves -= 1 + elif equiv_sets["wolf_fallen"] > 0: + equiv_sets["wolf_fallen"] -= 1 + equiv_sets["fallen_guardian"] = max(0, equiv_sets["fallen_guardian"] - 1) + rolecounts["fallen angel"][1] = max(0, rolecounts["fallen angel"][1] - 1) + rolecounts["guardian angel"][0] = max(rolecounts["guardian angel"][0] + 1, rolecounts["guardian angel"][1]) + rolecounts["fallen angel"][0] = min(rolecounts["fallen angel"][0], rolecounts["fallen angel"][1]) + elif pr == "fallen angel": + # person that died could have possibly been turned by alpha + if rolecounts[pr][1] == 0: + overkill = True + rolecounts[pr][0] = max(0, rolecounts[pr][0] - 1) + rolecounts[pr][1] = max(0, rolecounts[pr][1] - 1) + + if num_fallen > 0: + num_fallen -= 1 + elif equiv_sets["wolf_fallen"] > 0: + equiv_sets["wolf_fallen"] -= 1 + equiv_sets["fallen_guardian"] = max(0, equiv_sets["fallen_guardian"] - 1) + rolecounts["wolf"][1] = max(0, rolecounts["wolf"][1] - 1) + rolecounts["wolf"][0] = min(rolecounts["wolf"][0], rolecounts["wolf"][1]) + # this also means a GA died for sure (we lowered the lower bound previously) + rolecounts["guardian angel"][1] = max(0, rolecounts["guardian angel"][1] - 1) + elif pr == "guardian angel": + if rolecounts[pr][1] == 0: + overkill = True + if rolecounts[pr][1] <= equiv_sets["fallen_guardian"] and equiv_sets["fallen_guardian"] > 0: + # we got rid of a GA that was an FA candidate, so get rid of the FA as well + # (this also means that there is a guaranteed wolf so add that in) + equiv_sets["fallen_guardian"] = max(0, equiv_sets["fallen_guardian"] - 1) + equiv_sets["wolf_fallen"] = max(0, equiv_sets["wolf_fallen"] - 1) + rolecounts["fallen angel"][1] = max(rolecounts["fallen angel"][0], rolecounts["fallen angel"][1] - 1) + rolecounts["wolf"][0] = min(rolecounts["wolf"][0] + 1, rolecounts["wolf"][1]) + rolecounts[pr][0] = max(0, rolecounts[pr][0] - 1) + rolecounts[pr][1] = max(0, rolecounts[pr][1] - 1) + elif pr == "wolf cub": + if rolecounts[pr][1] == 0: + overkill = True + rolecounts[pr][0] = max(0, rolecounts[pr][0] - 1) + rolecounts[pr][1] = max(0, rolecounts[pr][1] - 1) + equiv_sets["amnesiac_clone_cub"] = max(0, equiv_sets["amnesiac_clone_cub"] - 1) + else: + # person that died is guaranteed to be that role (e.g. not in an equiv_set) + if rolecounts[pr][1] == 0: + overkill = True + rolecounts[pr][0] = max(0, rolecounts[pr][0] - 1) + rolecounts[pr][1] = max(0, rolecounts[pr][1] - 1) + + if overkill: + # we tried killing more people than exist in a role, so deduct from amnesiac/clone count instead + if pr == "clone": + # in this case, it means amnesiac became a clone (clone becoming amnesiac is impossible so we + # do not have the converse check in here - clones always inherit what amnesiac turns into). + equiv_sets["amnesiac_clone"] = max(0, equiv_sets["amnesiac_clone"] - 1) + equiv_sets["amnesiac_clone_cub"] = max(0, equiv_sets["amnesiac_clone_cub"] - 1) + rolecounts["amnesiac"][0] = max(0, rolecounts["amnesiac"][0] - 1) + rolecounts["amnesiac"][1] = max(0, rolecounts["amnesiac"][1] - 1) + elif pr == "wolf": + # This could potentially be caused by a cub, not necessarily amnesiac/clone + # as such we use a different equiv_set to reflect this + maybe_cub = True + num_realwolves = sum([rolecounts[r][1] for r in var.WOLF_ROLES if r != "wolf cub"]) + if rolecounts["wolf cub"][1] == 0 or num_realwolves > 0: + maybe_cub = False + + if (var.HIDDEN_AMNESIAC or rolecounts["amnesiac"][1] == 0) and (var.HIDDEN_CLONE or rolecounts["clone"][1] == 0): + # guaranteed to be cub + equiv_sets["amnesiac_clone_cub"] = max(0, equiv_sets["amnesiac_clone_cub"] - 1) + rolecounts["wolf cub"][0] = max(0, rolecounts["wolf cub"][0] - 1) + rolecounts["wolf cub"][1] = max(0, rolecounts["wolf cub"][1] - 1) + elif (var.HIDDEN_CLONE or rolecounts["clone"][1] == 0) and not maybe_cub: + # guaranteed to be amnesiac + equiv_sets["amnesiac_clone"] = max(0, equiv_sets["amnesiac_clone"] - 1) + equiv_sets["amnesiac_clone_cub"] = max(0, equiv_sets["amnesiac_clone_cub"] - 1) + rolecounts["amnesiac"][0] = max(0, rolecounts["amnesiac"][0] - 1) + rolecounts["amnesiac"][1] = max(0, rolecounts["amnesiac"][1] - 1) + elif (var.HIDDEN_AMNESIAC or rolecounts["amnesiac"][1] == 0) and not maybe_cub: + # guaranteed to be clone + equiv_sets["amnesiac_clone"] = max(0, equiv_sets["amnesiac_clone"] - 1) + equiv_sets["amnesiac_clone_cub"] = max(0, equiv_sets["amnesiac_clone_cub"] - 1) + rolecounts["clone"][0] = max(0, rolecounts["clone"][0] - 1) + rolecounts["clone"][1] = max(0, rolecounts["clone"][1] - 1) + else: + # could be anything, how exciting! + if maybe_cub: + maxcount = max(0, equiv_sets["amnesiac_clone_cub"] - 1) + else: + maxcount = max(0, equiv_sets["amnesiac_clone"] - 1) + + rolecounts["amnesiac"][0] = max(0, rolecounts["amnesiac"][0] - 1) + if rolecounts["amnesiac"][1] > maxcount: + rolecounts["amnesiac"][1] = maxcount + + rolecounts["clone"][0] = max(0, rolecounts["clone"][0] - 1) + if rolecounts["clone"][1] > maxcount: + rolecounts["clone"][1] = maxcount + + if maybe_cub: + rolecounts["wolf cub"][0] = max(0, rolecounts["wolf cub"][0] - 1) + if rolecounts["wolf cub"][1] > maxcount: + rolecounts["wolf cub"][1] = maxcount + + if maybe_cub: + equiv_sets["amnesiac_clone_cub"] = maxcount + equiv_sets["amnesiac_clone"] = min(equiv_sets["amnesiac_clone"], maxcount) + else: + equiv_sets["amnesiac_clone"] = maxcount + equiv_sets["amnesiac_clone_cub"] = max(maxcount, equiv_sets["amnesiac_clone_cub"] - 1) + + elif not var.HIDDEN_AMNESIAC and (var.HIDDEN_CLONE or rolecounts["clone"][1] == 0): + # guaranteed to be amnesiac overkilling as clone reports as clone + equiv_sets["amnesiac_clone"] = max(0, equiv_sets["amnesiac_clone"] - 1) + equiv_sets["amnesiac_clone_cub"] = max(0, equiv_sets["amnesiac_clone_cub"] - 1) + rolecounts["amnesiac"][0] = max(0, rolecounts["amnesiac"][0] - 1) + rolecounts["amnesiac"][1] = max(0, rolecounts["amnesiac"][1] - 1) + elif not var.HIDDEN_CLONE and (var.HIDDEN_AMNESIAC or rolecounts["amnesiac"][1] == 0): + # guaranteed to be clone overkilling as amnesiac reports as amnesiac + equiv_sets["amnesiac_clone"] = max(0, equiv_sets["amnesiac_clone"] - 1) + equiv_sets["amnesiac_clone_cub"] = max(0, equiv_sets["amnesiac_clone_cub"] - 1) + rolecounts["clone"][0] = max(0, rolecounts["clone"][0] - 1) + rolecounts["clone"][1] = max(0, rolecounts["clone"][1] - 1) + else: + # could be either + maxcount = max(0, equiv_sets["amnesiac_clone"] - 1) + + rolecounts["amnesiac"][0] = max(0, rolecounts["amnesiac"][0] - 1) + if rolecounts["amnesiac"][1] > maxcount: + rolecounts["amnesiac"][1] = maxcount + + rolecounts["clone"][0] = max(0, rolecounts["clone"][0] - 1) + if rolecounts["clone"][1] > maxcount: + rolecounts["clone"][1] = maxcount + + equiv_sets["amnesiac_clone"] = maxcount + equiv_sets["amnesiac_clone_cub"] = max(maxcount, equiv_sets["amnesiac_clone_cub"] - 1) + # Step 5. Handle cub growing up. Bot does not send out a message for this, so we need + # to puzzle it out ourselves. If there are no amnesiacs or clones + # then we can deterministically figure out cubs growing up. Otherwise we don't know for + # sure whether or not they grew up. + num_realwolves = sum([rolecounts[r][1] for r in var.WOLF_ROLES if r != "wolf cub"]) + if num_realwolves == 0: + # no wolves means cubs may have turned, set the min cub and max wolf appropriately + rolecounts["wolf"][1] += rolecounts["wolf cub"][1] + if rolecounts["amnesiac"][1] == 0 and rolecounts["clone"][1] == 0: + # we know for sure they grew up + rolecounts["wolf"][0] += rolecounts["wolf cub"][0] + rolecounts["wolf cub"][1] = 0 + rolecounts["wolf cub"][0] = 0 + # Finally, combine all of our rolecounts into a message, with the default role last + order = [r for r in var.role_order() if r in rolecounts] + if var.DEFAULT_ROLE in order: + order.remove(var.DEFAULT_ROLE) + order.append(var.DEFAULT_ROLE) + first = rolecounts[order[0]] + if first[0] == first[1] == 1: + vb = "is" else: - message.append("\u0002{0}\u0002 {1}".format(count, role)) + vb = "are" + + for role in order: + count = rolecounts[role] + if count[0] == count[1]: + if count[0] > 1 or count[0] == 0: + if count[0] == 0 and role not in start_roles: + continue + message.append("\u0002{0}\u0002 {1}".format(count[0] if count[0] else "\u0002no\u0002", var.plural(role))) + else: + message.append("\u0002{0}\u0002 {1}".format(count[0], role)) + else: + message.append("\u0002{0}-{1}\u0002 {2}".format(count[0], count[1], var.plural(role))) + + + # Show everything mostly as-is; the only hidden information is which + # role was turned into wolf due to alpha bite or lycanthropy totem. + # Amnesiac and clone show which roles they turned into. Time lords + # and VGs show individually instead of being lumped in the default role, + # and traitor is still based on var.HIDDEN_TRAITOR. + elif var.STATS_TYPE == "accurate": + l1 = [k for k in var.ROLES.keys() if var.ROLES[k]] + l2 = [k for k in var.ORIGINAL_ROLES.keys() if var.ORIGINAL_ROLES[k]] + rs = set(l1+l2) + rs = [role for role in var.role_order() if role in rs] + + # picky ordering: villager always last + if var.DEFAULT_ROLE in rs: + rs.remove(var.DEFAULT_ROLE) + rs.append(var.DEFAULT_ROLE) + + bitten_roles = defaultdict(int) + lycan_roles = defaultdict(int) + for role in var.BITTEN_ROLES.values(): + bitten_roles[role] += 1 + + for role in var.LYCAN_ROLES.values(): + lycan_roles[role] += 1 + + vb = "are" + for role in rs: + # only show actual roles + if role in var.TEMPLATE_RESTRICTIONS.keys(): + continue + count = len(var.ROLES[role]) + if role == "traitor" and var.HIDDEN_TRAITOR: + continue + elif role == var.DEFAULT_ROLE: + if var.HIDDEN_TRAITOR: + count += len(var.ROLES["traitor"]) + count += bitten_roles["traitor"] + count += lycan_roles["traitor"] + count += bitten_roles[var.DEFAULT_ROLE] + count += lycan_roles[var.DEFAULT_ROLE] + elif role == "wolf": + count -= sum(bitten_roles.values()) + count -= sum(lycan_roles.values()) + # GAs turn into FAs, not wolves for bitten_roles + # (but turn into wolves for lycan_roles) + count += bitten_roles["guardian angel"] + elif role == "fallen angel": + count -= bitten_roles["guardian angel"] + count += bitten_roles["fallen angel"] + count += lycan_roles["fallen angel"] + else: + count += bitten_roles[role] + count += lycan_roles[role] + + if role == rs[0]: + if count == 1: + vb = "is" + else: + vb = "are" + + if count != 1: + if count == 0 and len(var.ORIGINAL_ROLES[role]) == 0: + continue + message.append("\u0002{0}\u0002 {1}".format(count if count else "\u0002no\u0002", var.plural(role))) + else: + message.append("\u0002{0}\u0002 {1}".format(count, role)) + + # Only show team affiliation, this may be different than what mystics + # and wolf mystics are told since neutrals are split off. Determination + # of what numbers are shown is the same as summing up counts in "accurate" + elif var.STATS_TYPE == "team": + wolfteam = 0 + villagers = 0 + neutral = 0 + + for role, players in var.ROLES.items(): + if role in var.TEMPLATE_RESTRICTIONS.keys(): + continue + elif role in var.WOLFTEAM_ROLES: + if role == "traitor" and var.HIDDEN_TRAITOR: + villagers += len(players) + else: + wolfteam += len(players) + elif role in var.TRUE_NEUTRAL_ROLES: + neutral += len(players) + else: + villagers += len(players) + + for role in list(var.BITTEN_ROLES.values()) + list(var.LYCAN_ROLES.values()): + wolfteam -= 1 + if role in var.WOLFTEAM_ROLES: + if role == "traitor" and var.HIDDEN_TRAITOR: + villagers += 1 + else: + wolfteam += 1 + elif role in var.TRUE_NEUTRAL_ROLES: + neutral += 1 + else: + villagers += 1 + + message.append("\u0002{0}\u0002 {1}".format(wolfteam if wolfteam else "\u0002no\u0002", "wolf" if wolfteam == 1 else "wolves")) + message.append("\u0002{0}\u0002 {1}".format(villagers if villagers else "\u0002no\u0002", "villager" if villagers == 1 else "villagers")) + message.append("\u0002{0}\u0002 {1}".format(neutral if neutral else "\u0002no\u0002", "neutral player" if neutral == 1 else "neutral players")) + vb = "is" if wolfteam == 1 else "are" + stats_mssg = "{0}It is currently {4}. There {3} {1}, and {2}.".format(_nick, ", ".join(message[0:-1]), message[-1], @@ -1504,9 +1910,10 @@ def chk_decision(cli, force = ""): role = var.get_role(votee) if role == "amnesiac": var.ROLES["amnesiac"].remove(votee) - role = var.FINAL_ROLES[votee] + role = var.AMNESIAC_ROLES[votee] var.ROLES[role].append(votee) var.AMNESIACS.append(votee) + var.FINAL_ROLES[votee] = role pm(cli, votee, "Your totem clears your amnesia and you now fully remember who you are!") # If wolfteam, don't bother giving list of wolves since night is about to start anyway # Existing wolves also know that someone just joined their team because revealing totem says what they are @@ -1533,7 +1940,7 @@ def chk_decision(cli, force = ""): # Also kill the very last person to vote them, unless they voted themselves last in which case nobody else dies target = voters[-1] if target != votee: - if var.ROLE_REVEAL: + if var.ROLE_REVEAL in ("on", "team"): r1 = var.get_reveal_role(target) an1 = "n" if r1.startswith(("a", "e", "i", "o", "u")) else "" tmsg = ("As the noose is being fitted, \u0002{0}\u0002's totem emits a brilliant flash of light. " + @@ -1549,7 +1956,7 @@ def chk_decision(cli, force = ""): if votee in var.ROLES["jester"]: var.JESTERS.append(votee) - if var.ROLE_REVEAL: + if var.ROLE_REVEAL in ("on", "team"): rrole = var.get_reveal_role(votee) an = "n" if rrole.startswith(("a", "e", "i", "o", "u")) else "" lmsg = random.choice(var.LYNCH_MESSAGES).format(votee, an, rrole) @@ -1659,6 +2066,7 @@ def chk_traitor(cli): for wc in wcl: var.ROLES["wolf"].append(wc) var.ROLES["wolf cub"].remove(wc) + var.FINAL_ROLES[wc] = "wolf" pm(cli, wc, "You have grown up into a wolf and vowed to take revenge for your dead parents!") debuglog(wc, "(wolf cub) GROW UP") @@ -1666,14 +2074,15 @@ def chk_traitor(cli): for tt in ttl: var.ROLES["wolf"].append(tt) var.ROLES["traitor"].remove(tt) + var.FINAL_ROLES[tt] = "wolf" if tt in var.ROLES["cursed villager"]: var.ROLES["cursed villager"].remove(tt) pm(cli, tt, "HOOOOOOOOOWL. You have become... a wolf!\n"+ "It is up to you to avenge your fallen leaders!") debuglog(tt, "(traitor) TURNING") - # no message if wolf cub becomes wolf for now, may want to change that in future if len(var.ROLES["wolf"]) > 0: + var.TRAITOR_TURNED = True cli.msg(botconfig.CHANNEL, "\u0002The villagers, during their celebrations, are "+ "frightened as they hear a loud howl. The wolves are "+ "not gone!\u0002") @@ -1715,7 +2124,8 @@ def stop_game(cli, winner = "", abort = False): player = p #with (dced) still in if p.startswith("(dced)"): p = p[6:] - if p in var.FINAL_ROLES and var.FINAL_ROLES[p] != role and (role != "amnesiac" or p in var.AMNESIACS): + # Show cubs and traitors as themselves even if they turned into wolf + if p in var.FINAL_ROLES and var.FINAL_ROLES[p] != role and (var.FINAL_ROLES[p] != "wolf" or role not in ("wolf cub", "traitor")): origroles[p] = role rolelist[role].remove(player) rolelist[var.FINAL_ROLES[p]].append(p) @@ -1774,9 +2184,7 @@ def stop_game(cli, winner = "", abort = False): continue for x in ppl: if x != None: - if role == "amnesiac" and x in var.AMNESIACS: - plrl[x] = var.FINAL_ROLES[x] - elif role != "amnesiac" and x in var.FINAL_ROLES: # role swap or clone + if x in var.FINAL_ROLES: plrl[x] = var.FINAL_ROLES[x] else: plrl[x] = role @@ -2074,7 +2482,7 @@ def del_player(cli, nick, forced_death = False, devoice = True, end_game = True, var.ROLES["clone"].remove(clone) if nickrole == "amnesiac": # clone gets the amnesiac's real role - sayrole = var.FINAL_ROLES[nick] + sayrole = var.AMNESIAC_ROLES[nick] var.FINAL_ROLES[clone] = sayrole var.ROLES[sayrole].append(clone) else: @@ -2131,7 +2539,7 @@ def del_player(cli, nick, forced_death = False, devoice = True, end_game = True, if nick not in var.LOVERS[other]: continue var.LOVERS[other].remove(nick) - if var.ROLE_REVEAL: + if var.ROLE_REVEAL in ("on", "team"): role = var.get_reveal_role(other) an = "n" if role.startswith(("a", "e", "i", "o", "u")) else "" message = ("Saddened by the loss of their lover, \u0002{0}\u0002, " + @@ -2168,7 +2576,7 @@ def del_player(cli, nick, forced_death = False, devoice = True, end_game = True, pl.remove(ga) break else: - if var.ROLE_REVEAL: + if var.ROLE_REVEAL in ("on", "team"): role = var.get_reveal_role(target) an = "n" if role.startswith(("a", "e", "i", "o", "u")) else "" message = ("Before dying, \u0002{0}\u0002 quickly slits \u0002{1}\u0002's throat. " + @@ -2281,7 +2689,7 @@ def del_player(cli, nick, forced_death = False, devoice = True, end_game = True, if target1 in pl: if target2 in pl and target1 != target2: - if var.ROLE_REVEAL: + if var.ROLE_REVEAL in ("on", "team"): r1 = var.get_reveal_role(target1) an1 = "n" if r1.startswith(("a", "e", "i", "o", "u")) else "" r2 = var.get_reveal_role(target2) @@ -2305,7 +2713,7 @@ def del_player(cli, nick, forced_death = False, devoice = True, end_game = True, pl.remove(target1) pl.remove(target2) else: - if var.ROLE_REVEAL: + if var.ROLE_REVEAL in ("on", "team"): r1 = var.get_reveal_role(target1) an1 = "n" if r1.startswith(("a", "e", "i", "o", "u")) else "" tmsg = ("\u0002{0}\u0002 throws " + @@ -2321,7 +2729,7 @@ def del_player(cli, nick, forced_death = False, devoice = True, end_game = True, pl.remove(target1) else: if target2 in pl: - if var.ROLE_REVEAL: + if var.ROLE_REVEAL in ("on", "team"): r2 = var.get_reveal_role(target2) an2 = "n" if r2.startswith(("a", "e", "i", "o", "u")) else "" tmsg = ("\u0002{0}\u0002 throws " + @@ -2455,7 +2863,7 @@ def reaper(cli, gameid): for nck in to_kill: if nck not in var.list_players(): continue - if var.ROLE_REVEAL: + if var.ROLE_REVEAL in ("on", "team"): cli.msg(chan, ("\u0002{0}\u0002 didn't get out of bed for a very long "+ "time and has been found dead. The survivors bury "+ "the \u0002{1}\u0002's body.").format(nck, var.get_reveal_role(nck))) @@ -2481,7 +2889,7 @@ def reaper(cli, gameid): for dcedplayer in list(var.DISCONNECTED.keys()): acc, cloak, timeofdc, what = var.DISCONNECTED[dcedplayer] if what in ("quit", "badnick") and (datetime.now() - timeofdc) > timedelta(seconds=var.QUIT_GRACE_TIME): - if var.get_role(dcedplayer) != "person" and var.ROLE_REVEAL: + if var.get_role(dcedplayer) != "person" and var.ROLE_REVEAL in ("on", "team"): cli.msg(chan, ("\u0002{0}\u0002 was mauled by wild animals and has died. It seems that "+ "\u0002{1}\u0002 meat is tasty.").format(dcedplayer, var.get_reveal_role(dcedplayer))) else: @@ -2491,7 +2899,7 @@ def reaper(cli, gameid): if not del_player(cli, dcedplayer, devoice = False, death_triggers = False): return elif what == "part" and (datetime.now() - timeofdc) > timedelta(seconds=var.PART_GRACE_TIME): - if var.get_role(dcedplayer) != "person" and var.ROLE_REVEAL: + if var.get_role(dcedplayer) != "person" and var.ROLE_REVEAL in ("on", "team"): cli.msg(chan, ("\u0002{0}\u0002, a \u0002{1}\u0002, ate some poisonous berries "+ "and has died.").format(dcedplayer, var.get_reveal_role(dcedplayer))) else: @@ -2501,7 +2909,7 @@ def reaper(cli, gameid): if not del_player(cli, dcedplayer, devoice = False, death_triggers = False): return elif what == "account" and (datetime.now() - timeofdc) > timedelta(seconds=var.ACC_GRACE_TIME): - if var.get_role(dcedplayer) != "person" and var.ROLE_REVEAL: + if var.get_role(dcedplayer) != "person" and var.ROLE_REVEAL in ("on", "team"): cli.msg(chan, ("\u0002{0}\u0002 has died of a heart attack. The villagers "+ "couldn't save the \u0002{1}\u0002.").format(dcedplayer, var.get_reveal_role(dcedplayer))) else: @@ -2654,7 +3062,9 @@ def on_nick(cli, oldnick, nick): if prefix == k: var.PLAYERS[nick] = var.PLAYERS[k] del var.PLAYERS[k] - 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, var.SHAMANS): + 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.SHAMANS): kvp = [] for a,b in dictvar.items(): if a == prefix: @@ -2665,7 +3075,8 @@ 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, var.BITTEN, var.GUNNERS, var.DOCTORS, var.TURNCOATS): + for dictvar in (var.VENGEFUL_GHOSTS, var.TOTEMS, var.FINAL_ROLES, var.BITTEN, var.GUNNERS, var.TURNCOATS, + var.DOCTORS, var.BITTEN_ROLES, var.LYCAN_ROLES, var.AMNESIAC_ROLES): if prefix in dictvar.keys(): dictvar[nick] = dictvar[prefix] del dictvar[prefix] @@ -2683,6 +3094,13 @@ def on_nick(cli, oldnick, nick): dictvar.update(kvp) if prefix in dictvar.keys(): del dictvar[prefix] + for idx, tup in enumerate(var.EXCHANGED_ROLES): + a, b = tup + if a == prefix: + a = nick + if b == prefix: + b = nick + var.EXCHANGED_ROLES[idx] = (a, b) if prefix in var.SEEN: var.SEEN.remove(prefix) var.SEEN.append(nick) @@ -2889,19 +3307,19 @@ def leave(cli, what, nick, why=""): population = (" New player count: \u0002{0}\u0002").format(lpl) if what == "part" and (not var.PART_GRACE_TIME or var.PHASE == "join"): - if var.get_role(nick) != "person" and var.ROLE_REVEAL: + if var.get_role(nick) != "person" and var.ROLE_REVEAL in ("on", "team"): msg = ("\u0002{0}\u0002, a \u0002{1}\u0002, ate some poisonous berries and has "+ "died.{2}").format(nick, var.get_reveal_role(nick), population) else: msg = ("\u0002{0}\u0002 ate some poisonous berries and has died.{1}").format(nick, population) elif what in ("quit", "badnick") and (not var.QUIT_GRACE_TIME or var.PHASE == "join"): - if var.get_role(nick) != "person" and var.ROLE_REVEAL: + if var.get_role(nick) != "person" and var.ROLE_REVEAL in ("on", "team"): msg = ("\u0002{0}\u0002 was mauled by wild animals and has died. It seems that "+ "\u0002{1}\u0002 meat is tasty.{2}").format(nick, var.get_reveal_role(nick), population) else: msg = ("\u0002{0}\u0002 was mauled by wild animals and has died.{1}").format(nick, population) elif what == "account" and (not var.ACC_GRACE_TIME or var.PHASE == "join"): - if var.get_role(nick) != "person" and var.ROLE_REVEAL: + if var.get_role(nick) != "person" and var.ROLE_REVEAL in ("on", "team"): msg = ("\u0002{0}\u0002 fell into a river and was swept away. The villagers couldn't "+ "save the \u0002{1}\u0002.{2}").format(nick, var.get_reveal_role(nick), population) else: @@ -2910,7 +3328,7 @@ def leave(cli, what, nick, why=""): msg = "\u0002{0}\u0002 has gone missing.".format(nick) killplayer = False else: - if var.get_role(nick) != "person" and var.ROLE_REVEAL: + if var.get_role(nick) != "person" and var.ROLE_REVEAL in ("on", "team"): msg = ("\u0002{0}\u0002 died due to falling off a cliff. The "+ "\u0002{1}\u0002 is lost to the ravine forever.{2}").format(nick, var.get_reveal_role(nick), population) else: @@ -2947,7 +3365,7 @@ def leave_game(cli, nick, chan, rest): cli.notice(nick, "The game already started! If you still want to quit, try again in {0} second{1}.".format(dur, "" if dur == 1 else "s")) return population = "" - if var.get_role(nick) != "person" and var.ROLE_REVEAL: + if var.get_role(nick) != "person" and var.ROLE_REVEAL in ("on", "team"): role = var.get_reveal_role(nick) an = "n" if role.startswith(("a", "e", "i", "o", "u")) else "" if var.DYNQUIT_DURING_GAME: @@ -3094,9 +3512,6 @@ def transition_day(cli, gameid=0): if clone not in var.CLONED: ps = pl[:] ps.remove(clone) - for victim in victims: - if victim in ps: - ps.remove(victim) if len(ps) > 0: target = random.choice(ps) var.CLONED[clone] = target @@ -3481,6 +3896,7 @@ def transition_day(cli, gameid=0): novictmsg = True if new_wolf: message.append("A chilling howl was heard last night. It appears there is another werewolf in our midst!") + var.EXTRA_WOLVES += 1 novictmsg = False for victim in vlist: @@ -3506,7 +3922,9 @@ def transition_day(cli, gameid=0): vrole = var.get_role(victim) if vrole not in var.WOLFCHAT_ROLES: message.append("A chilling howl was heard last night. It appears there is another werewolf in our midst!") + var.EXTRA_WOLVES += 1 pm(cli, victim, "HOOOOOOOOOWL. You have become... a wolf!") + var.LYCAN_ROLES[victim] = vrole var.ROLES[vrole].remove(victim) var.ROLES["wolf"].append(victim) var.FINAL_ROLES[victim] = "wolf" @@ -3539,7 +3957,7 @@ def transition_day(cli, gameid=0): "It appears that \u0002{1}\u0002's spirit was driven away by the flash.").format(victim, loser)) else: dead.append(loser) - if var.ROLE_REVEAL: + if var.ROLE_REVEAL in ("on", "team"): role = var.get_reveal_role(loser) an = "n" if role.startswith(("a", "e", "i", "o", "u")) else "" message.append(("\u0002{0}\u0002's totem emitted a brilliant flash of light last night. " + @@ -3547,7 +3965,7 @@ def transition_day(cli, gameid=0): else: message.append(("\u0002{0}\u0002's totem emitted a brilliant flash of light last night. " + "The dead body of \u0002{1}\u0002 was found at the scene.").format(victim, loser)) - if var.ROLE_REVEAL: + if var.ROLE_REVEAL in ("on", "team"): role = var.get_reveal_role(victim) an = "n" if role.startswith(("a", "e", "i", "o", "u")) else "" message.append(("The dead body of \u0002{0}\u0002, a{1} \u0002{2}\u0002, is found. " + @@ -3607,11 +4025,11 @@ def transition_day(cli, gameid=0): onlybywolves.add(bodyguard) r = random.random() if r < var.BODYGUARD_DIES_CHANCE: - if var.ROLE_REVEAL: + if var.ROLE_REVEAL == "on": message.append(("\u0002{0}\u0002, a \u0002bodyguard\u0002, "+ "made the unfortunate mistake of guarding a wolf "+ "last night, and is now dead.").format(bodyguard)) - else: + else: # off and team message.append(("\u0002{0}\u0002 "+ "made the unfortunate mistake of guarding a wolf "+ "last night, and is now dead.").format(bodyguard)) @@ -3622,11 +4040,11 @@ def transition_day(cli, gameid=0): onlybywolves.add(gangel) r = random.random() if r < var.GUARDIAN_ANGEL_DIES_CHANCE: - if var.ROLE_REVEAL: + if var.ROLE_REVEAL == "on": message.append(("\u0002{0}\u0002, a \u0002guardian angel\u0002, "+ "made the unfortunate mistake of guarding a wolf "+ "last night, and is now dead.").format(gangel)) - else: + else: # off and team message.append(("\u0002{0}\u0002 "+ "made the unfortunate mistake of guarding a wolf "+ "last night, and is now dead.").format(gangel)) @@ -3639,7 +4057,7 @@ def transition_day(cli, gameid=0): killlist = [wolf for wolf in var.list_players(var.WOLF_ROLES) if wolf not in var.OBSERVED.keys() and wolf not in dead] if killlist: deadwolf = random.choice(killlist) - if var.ROLE_REVEAL: + if var.ROLE_REVEAL in ("on", "team"): message.append(("Fortunately, \u0002{0}\u0002 had bullets and "+ "\u0002{1}\u0002, a \u0002{2}\u0002, was shot dead.").format(victim, deadwolf, var.get_reveal_role(deadwolf))) else: @@ -3930,7 +4348,13 @@ def check_exchange(cli, actor, nick): nick_role = var.get_role(nick) if actor_role == "amnesiac": - actor_role = var.FINAL_ROLES[actor] + actor_role = var.AMNESIAC_ROLES[actor] + if nick in var.AMNESIAC_ROLES: + var.AMNESIAC_ROLES[actor] = var.AMNESIAC_ROLES[nick] + var.AMNESIAC_ROLES[nick] = actor_role + else: + del var.AMNESIAC_ROLES[actor] + var.AMNESIAC_ROLES[nick] = actor_role elif actor_role == "clone": if actor in var.CLONED: actor_target = var.CLONED[actor] @@ -4000,7 +4424,12 @@ def check_exchange(cli, actor, nick): del var.TURNCOATS[actor] if nick_role == "amnesiac": - nick_role = var.FINAL_ROLES[nick] + if actor not in var.AMNESIAC_ROLES: + nick_role = var.AMNESIAC_ROLES[nick] + var.AMNESIAC_ROLES[actor] = nick_role + del var.AMNESIAC_ROLES[nick] + else: # we swapped amnesiac_roles earlier on, get our version back + nick_role = var.AMNESIAC_ROLES[actor] elif nick_role == "clone": if nick in var.CLONED: nick_target = var.CLONED[nick] @@ -4074,9 +4503,24 @@ def check_exchange(cli, actor, nick): 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 + if nick in var.BITTEN_ROLES.keys(): + var.BITTEN_ROLES[actor], var.BITTEN_ROLES[nick] = var.BITTEN_ROLES[nick], var.BITTEN_ROLES[actor] + else: + var.BITTEN_ROLES[nick] = var.BITTEN_ROLES[actor] + del var.BITTEN_ROLES[actor] + elif nick in var.BITTEN_ROLES.keys(): + var.BITTEN_ROLES[actor] = var.BITTEN_ROLES[nick] + del var.BITTEN_ROLES[nick] + + if actor in var.LYCAN_ROLES.keys(): + if nick in var.LYCAN_ROLES.keys(): + var.LYCAN_ROLES[actor], var.LYCAN_ROLES[nick] = var.LYCAN_ROLES[nick], var.LYCAN_ROLES[actor] + else: + var.LYCAN_ROLES[nick] = var.LYCAN_ROLES[actor] + del var.LYCAN_ROLES[actor] + elif nick in var.LYCAN_ROLES.keys(): + var.LYCAN_ROLES[actor] = var.LYCAN_ROLES[nick] + del var.LYCAN_ROLES[nick] actor_rev_role = actor_role if actor_role == "vengeful ghost": @@ -4181,6 +4625,7 @@ def check_exchange(cli, actor, nick): elif actor_role == "turncoat": var.TURNCOATS[nick] = ("none", -1) + var.EXCHANGED_ROLES.append((actor, nick)) return True return False @@ -4298,10 +4743,10 @@ def shoot(cli, nick, chan, rest): "a silver bullet!").format(nick, victim)) an = "n" if victimrole.startswith(("a", "e", "i", "o", "u")) else "" if realrole in var.WOLF_ROLES: - if var.ROLE_REVEAL: + if var.ROLE_REVEAL == "on": cli.msg(chan, ("\u0002{0}\u0002 is a{1} \u0002{2}\u0002, and is dying from "+ "the silver bullet.").format(victim,an, victimrole)) - else: + else: # off and team cli.msg(chan, ("\u0002{0}\u0002 is a wolf, and is dying from "+ "the silver bullet.").format(victim)) if not del_player(cli, victim, killer_role = var.get_role(nick)): @@ -4312,7 +4757,7 @@ def shoot(cli, nick, chan, rest): accident = "" # it's an accident if the sharpshooter DOESN'T headshot :P cli.msg(chan, ("\u0002{0}\u0002 is not a wolf "+ "but was {1}fatally injured.").format(victim, accident)) - if var.ROLE_REVEAL: + if var.ROLE_REVEAL in ("on", "team"): cli.msg(chan, "The village has sacrificed a{0} \u0002{1}\u0002.".format(an, victimrole)) if not del_player(cli, victim, killer_role = var.get_role(nick)): return @@ -4334,7 +4779,7 @@ def shoot(cli, nick, chan, rest): elif rand <= chances[0] + chances[1]: cli.msg(chan, "\u0002{0}\u0002 is a lousy shooter and missed!".format(nick)) else: - if var.ROLE_REVEAL: + if var.ROLE_REVEAL in ("on", "team"): cli.msg(chan, ("Oh no! \u0002{0}\u0002's gun was poorly maintained and has exploded! "+ "The village mourns a gunner-\u0002{1}\u0002.").format(nick, var.get_reveal_role(nick))) else: @@ -4538,7 +4983,7 @@ def observe(cli, nick, chan, rest): elif role == "sorcerer": vrole = var.get_role(victim) if vrole == "amnesiac": - vrole = var.FINAL_ROLES[victim] + vrole = var.AMNESIAC_ROLES[victim] if vrole in ("seer", "oracle", "augur", "sorcerer"): an = "n" if vrole.startswith(("a", "e", "i", "o", "u")) else "" pm(cli, nick, ("After casting your ritual, you determine that \u0002{0}\u0002 " + @@ -4567,7 +5012,7 @@ def investigate(cli, nick, chan, rest): var.INVESTIGATED.append(nick) vrole = var.get_role(victim) if vrole == "amnesiac": - vrole = var.FINAL_ROLES[victim] + vrole = var.AMNESIAC_ROLES[victim] pm(cli, nick, ("The results of your investigation have returned. \u0002{0}\u0002"+ " is a... \u0002{1}\u0002!").format(victim, vrole)) debuglog("{0} ({1}) ID: {2} ({3})".format(nick, var.get_role(nick), victim, vrole)) @@ -4648,7 +5093,7 @@ def see(cli, nick, chan, rest): debuglog("{0} ({1}) SEE: {2} ({3}) (Wolf: {4})".format(nick, role, victim, vrole, str(iswolf))) elif role == "augur": if victimrole == "amnesiac": - victimrole = var.FINAL_ROLES[victim] + victimrole = var.AMNESIAC_ROLES[victim] aura = "blue" if victimrole in var.WOLFTEAM_ROLES: aura = "red" @@ -5390,11 +5835,12 @@ def transition_night(cli): for amn in amns: event = Event("amnesiac_turn", {}) - if event.dispatch(var, amn, var.FINAL_ROLES[amn]): - amnrole = var.FINAL_ROLES[amn] + if event.dispatch(var, amn, var.AMNESIAC_ROLES[amn]): + amnrole = var.AMNESIAC_ROLES[amn] var.ROLES["amnesiac"].remove(amn) var.ROLES[amnrole].append(amn) var.AMNESIACS.append(amn) + var.FINAL_ROLES[amn] = amnrole if var.FIRST_NIGHT: # we don't need to tell them twice if they remember right away continue showrole = amnrole @@ -6147,6 +6593,7 @@ def start(cli, nick, chan, forced = False, restart = ""): var.DAY_COUNT = 0 var.ANGRY_WOLVES = False var.DISEASED_WOLVES = False + var.TRAITOR_TURNED = False var.FINAL_ROLES = {} var.ORIGINAL_LOVERS = {} var.IMPATIENT = [] @@ -6176,10 +6623,14 @@ def start(cli, nick, chan, forced = False, restart = ""): var.BITTEN = {} var.BITE_PREFERENCES = {} var.BITTEN_ROLES = {} + var.LYCAN_ROLES = {} + var.AMNESIAC_ROLES = {} var.CHARMERS = set() var.CHARMED = set() var.ACTIVE_PROTECTIONS = defaultdict(list) var.TURNCOATS = {} + var.EXCHANGED_ROLES = [] + var.EXTRA_WOLVES = 0 for role, count in addroles.items(): if role in var.TEMPLATE_RESTRICTIONS.keys(): @@ -6255,20 +6706,52 @@ def start(cli, nick, chan, forced = False, restart = ""): if not restart: gamemode = var.CURRENT_GAMEMODE.name - if gamemode == "random": - if var.ROLE_REVEAL == "partial": - gamemode = "random_reveal" + # Alert the players to option changes they may not be aware of + options = [] + if var.ORIGINAL_SETTINGS.get("ROLE_REVEAL") is not None: + if var.ROLE_REVEAL == "on": + options.append("role reveal") + elif var.ROLE_REVEAL == "team": + options.append("team reveal") + elif var.ROLE_REVEAL == "off": + options.append("no reveal") + if var.ORIGINAL_SETTINGS.get("STATS_TYPE") is not None: + if var.STATS_TYPE == "disabled": + options.append("no stats") else: - gamemode = "random_noreveal" + options.append("{0} stats".format(var.STATS_TYPE)) + if var.ORIGINAL_SETTINGS.get("ABSTAIN_ENABLED") is not None or var.ORIGINAL_SETTINGS.get("LIMIT_ABSTAIN") is not None: + if var.ABSTAIN_ENABLED and var.LIMIT_ABSTAIN: + options.append("restricted abstaining") + elif var.ABSTAIN_ENABLED: + options.append("unrestricted abstaining") + else: + options.append("no abstaining") + + if len(options) > 2: + options = " with {0}, and {1}".format(", ".join(options[:-1]), options[-1]) + elif len(options) == 2: + options = " with {0} and {1}".format(options[0], options[1]) + elif len(options) == 1: + options = " with {0}".format(options[0]) + else: + options = "" cli.msg(chan, ("{0}: Welcome to Werewolf, the popular detective/social party "+ - "game (a theme of Mafia). Using the \u0002{1}\u0002 game mode.").format(", ".join(pl), gamemode)) + "game (a theme of Mafia). Using the \u0002{1}\u0002 game mode{2}.").format(", ".join(pl), gamemode, options)) cli.mode(chan, "+m") var.ORIGINAL_ROLES = copy.deepcopy(var.ROLES) # Make a copy - # Handle amnesiac - amnroles = list(var.ROLE_GUIDE.keys() - [var.DEFAULT_ROLE, "amnesiac"]) + # Handle amnesiac; + # matchmaker is blacklisted if AMNESIAC_NIGHTS > 1 due to only being able to act night 1 + # clone and traitor are blacklisted due to assumptions made in default !stats computations. + # If you remove these from the blacklist you will need to modify the default !stats logic + # chains in order to correctly account for these. As a forewarning, such modifications are + # nontrivial and will likely require a great deal of thought (and likely new tracking vars) + amnroles = list(var.ROLE_GUIDE.keys() - [var.DEFAULT_ROLE, "amnesiac", "clone", "traitor"]) + if var.AMNESIAC_NIGHTS > 1 and "matchmaker" in amnroles: + amnroles.remove("matchmaker") for nope in var.AMNESIAC_BLACKLIST: if nope in amnroles: amnroles.remove(nope) @@ -6276,13 +6759,13 @@ def start(cli, nick, chan, forced = False, restart = ""): if nope in amnroles: amnroles.remove(nope) for amnesiac in var.ROLES["amnesiac"]: - var.FINAL_ROLES[amnesiac] = random.choice(amnroles) + var.AMNESIAC_ROLES[amnesiac] = random.choice(amnroles) # Handle doctor for doctor in var.ROLES["doctor"]: var.DOCTORS[doctor] = math.ceil(var.DOCTOR_IMMUNIZATION_MULTIPLIER * len(pl)) - for amn in var.FINAL_ROLES: - if var.FINAL_ROLES[amn] == "doctor": + for amn in var.AMNESIAC_ROLES: + if var.AMNESIAC_ROLES[amn] == "doctor": var.DOCTORS[amn] = math.ceil(var.DOCTOR_IMMUNIZATION_MULTIPLIER * len(pl)) var.DAY_TIMEDELTA = timedelta(0) @@ -7616,8 +8099,8 @@ if botconfig.DEBUG_MODE or botconfig.ALLOWED_NORMAL_MODE_COMMANDS: nicks[i] += " (gave {0} totem to {1})".format(var.TOTEMS[nickname], var.LASTGIVEN[nickname]) elif role == "clone" and nickname in var.CLONED: nicks[i] += " (cloned {0})".format(var.CLONED[nickname]) - elif role == "amnesiac" and nickname in var.FINAL_ROLES: - nicks[i] += " (will become {0})".format(var.FINAL_ROLES[nickname]) + elif role == "amnesiac" and nickname in var.AMNESIAC_ROLES: + nicks[i] += " (will become {0})".format(var.AMNESIAC_ROLES[nickname]) # print how many bullets normal gunners have elif (role == "gunner" or role == "sharpshooter") and nickname in var.GUNNERS: nicks[i] += " ({0} bullet{1})".format(var.GUNNERS[nickname], "" if var.GUNNERS[nickname] == 1 else "s") @@ -7685,26 +8168,21 @@ if botconfig.DEBUG_MODE or botconfig.ALLOWED_NORMAL_MODE_COMMANDS: if rest: gamemode = rest.strip().lower() + parts = gamemode.split("=", 2) + if len(parts) > 1: + gamemode, modeargs = parts + else: + gamemode = parts[0] + modeargs = None - force_reveal = None - - if gamemode == "random_reveal": - gamemode = "random" - force_reveal = "partial" - elif gamemode == "random_noreveal": - gamemode = "random" - force_reveal = False - - if gamemode not in var.GAME_MODES.keys() and not gamemode.startswith("roles"): + if gamemode not in var.GAME_MODES.keys(): gamemode = gamemode.split()[0] gamemode, _ = complete_match(gamemode, var.GAME_MODES.keys()) if not gamemode: cli.notice(nick, "\u0002{0}\u0002 is not a valid game mode.".format(rest)) return - if cgamemode(cli, gamemode): - if force_reveal is not None: - var.ROLE_REVEAL = force_reveal + if cgamemode(cli, "=".join(parts)): cli.msg(chan, ("\u0002{0}\u0002 has changed the game settings " "successfully.").format(nick)) var.FGAMED = True @@ -7894,6 +8372,11 @@ if botconfig.DEBUG_MODE or botconfig.ALLOWED_NORMAL_MODE_COMMANDS: return cli.msg(chan, "Operation successful.") if var.PHASE not in ("none", "join"): + # default stats determination does not work if we're mucking with !frole + if var.STATS_TYPE == "default": + var.ORIGINAL_SETTINGS["STATS_TYPE"] = var.STATS_TYPE + var.STATS_TYPE = "accurate" + cli.msg(chan, "!stats type changed to accurate due to use of !frole.") chk_win(cli)