diff --git a/messages/en.json b/messages/en.json index 1c77e84..82b5880 100644 --- a/messages/en.json +++ b/messages/en.json @@ -186,7 +186,7 @@ "forever_aclone": "It appears that \u0002{0}\u0002 was cloning you, so you are now stuck as a clone forever. How sad.", "clone_success": "You will now be cloning \u0002{0}\u0002 if they die.", "clone_wolf": "\u0002{0}\u0002 cloned \u0002{1}\u0002 and has now become a wolf!", - "no_other_wolves": "There are no other wolves", + "no_other_wolves": "There are no other wolves.", "lover_suicide": "Saddened by the loss of their lover, \u0002{0}\u0002, a{1} \u0002{2}\u0002, commits suicide.", "lover_suicide_no_reveal": "Saddened by the loss of their lover, \u0002{0}\u0002 commits suicide.", "assassin_fail_totem": "Before dying, \u0002{0}\u0002 quickly attempts to slit \u0002{1}\u0002's throat; however, {1}'s totem emits a brilliant flash of light, causing the attempt to miss.", @@ -203,6 +203,12 @@ "mad_scientist_kill_single_no_reveal": "\u0002{0}\u0002 throws a potent chemical concoction into the crowd. \u0002{1}\u0002 gets hit by the chemicals and dies.", "mad_scientist_fail": "\u0002{0}\u0002 throws a potent chemical concoction into the crowd. Thankfully, nobody seems to have gotten hit.", "hunter_discard": "Your target has died, so you may now pick a new one.", + "wild_child_already_picked": "You have already picked your idol for this game.", + "wild_child_success": "You have picked {0} to be your idol for this game.", + "wild_child_as_wolf": "\u0002{0}\u0002's idol has died, and is now a \u0002wolf\u0002!", + "wild_child_random_idol": "Upon waking up, the first person you see is \u0002{0}\u0002, and they become your idol.", + "idol_died": "Your idol has died, and you are now a \u0002wolf\u0002!", + "wild_child_idol": "Your idol is \u0002{0}\u0002.", "idle_death": "\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.", "idle_death_no_reveal": "\u0002{0}\u0002 didn't get out of bed for a very long time and has been found dead.", "channel_idle_warning": "{0}: \u0002You have been idling for a while. Please say something soon or you might be declared dead.\u0002", @@ -511,6 +517,8 @@ "monster_simple": "You are a \u0002monster\u0002.", "lycan_notify": "You are a \u0002lycan\u0002. You are currently on the side of the villagers, but will turn into a wolf instead of dying if you are targeted by the wolves during the night.", "lycan_simple": "You are a \u0002lycan\u0002.", + "child_notify": "You are a \u0002wild child\u0002. You must pick an idol with \"choose \", and you will become a wolf if your idol dies. You are a villager as long as your idol is alive.", + "child_simple": "You are a \u0002wild child\u0002.", "vengeful_ghost_notify": "You are a \u0002vengeful ghost\u0002, sworn to take revenge on the {0} that you believe killed you. You must kill one of them with \"kill \" tonight. If you do not, one of them will be selected at random.", "vengeful_ghost_simple": "You are a \u0002vengeful ghost\u0002.", "drunken_assassin_notification": "You are an \u0002assassin\u0002. In your drunken stupor you have selected \u0002{0}\u0002 as your target.", diff --git a/src/settings.py b/src/settings.py index 6c24edc..cbf5318 100644 --- a/src/settings.py +++ b/src/settings.py @@ -245,6 +245,7 @@ ROLE_GUIDE = OrderedDict([ # This is order-sensitive - many parts of the code re ("turncoat" , ( 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 )), ("clone" , ( 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 )), ("lycan" , ( 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 )), + ("wild child" , ( 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 )), ("succubus" , ( 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 )), ("demoniac" , ( 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 )), @@ -269,7 +270,7 @@ WOLFTEAM_ROLES = WOLFCHAT_ROLES | {"minion", "cultist"} # These roles either steal away wins or can otherwise win with any team TRUE_NEUTRAL_ROLES = frozenset({"crazed shaman", "fool", "jester", "monster", "clone", "piper", "turncoat", "succubus", "demoniac", "dullahan"}) # These are the roles that will NOT be used for when amnesiac turns, everything else is fair game! (var.DEFAULT_ROLE is also added if not in this set) -AMNESIAC_BLACKLIST = frozenset({"monster", "demoniac", "minion", "matchmaker", "clone", "doctor", "villager", "cultist", "piper", "dullahan"}) +AMNESIAC_BLACKLIST = frozenset({"monster", "demoniac", "minion", "matchmaker", "clone", "doctor", "villager", "cultist", "piper", "dullahan", "wild child"}) # These roles are seen as wolf by the seer/oracle SEEN_WOLF = WOLF_ROLES | {"monster", "mad scientist", "succubus"} # These are seen as the default role (or villager) when seen by seer (this overrides SEEN_WOLF) @@ -283,10 +284,10 @@ BENEFICIAL_TOTEMS = frozenset({"protection", "revealing", "desperation", "influe # NB: if you want a template to apply to everyone, list it here but make the restrictions an empty set. Templates not listed here are considered full roles instead TEMPLATE_RESTRICTIONS = OrderedDict([ ("cursed villager" , SEEN_WOLF | {"seer", "oracle", "fool", "jester", "priest"}), - ("gunner" , WOLFTEAM_ROLES | {"fool", "lycan", "jester", "priest"}), + ("gunner" , WOLFTEAM_ROLES | {"fool", "lycan", "jester", "priest", "wild child"}), ("sharpshooter" , frozenset()), # the above gets automatically added to the set. this set is the list of roles that can be gunner but not sharpshooter ("mayor" , frozenset({"fool", "jester", "monster"})), - ("assassin" , WOLF_ROLES | {"traitor", "seer", "augur", "oracle", "harlot", "detective", "bodyguard", "guardian angel", "lycan", "priest"}), + ("assassin" , WOLF_ROLES | {"traitor", "seer", "augur", "oracle", "harlot", "detective", "bodyguard", "guardian angel", "lycan", "priest", "wild child"}), ("bureaucrat" , frozenset()), ("blessed villager" , frozenset(ROLE_GUIDE.keys()) - {"villager", "blessed villager", "mayor", "bureaucrat"}), ]) @@ -403,7 +404,8 @@ def plural(role, count=2): bits[-1] = {"person": "people", "wolf": "wolves", "has": "have", - "succubus": "succubi"}.get(bits[-1], bits[-1] + "s") + "succubus": "succubi", + "child": "children"}.get(bits[-1], bits[-1] + "s") return " ".join(bits) def list_players(roles = None): @@ -440,6 +442,8 @@ def get_reveal_role(nick): role = "amnesiac" elif HIDDEN_CLONE and nick in ORIGINAL_ROLES["clone"]: role = "clone" + elif nick in WILD_CHILDREN: + role = "wild child" else: role = get_role(nick) diff --git a/src/wolfgame.py b/src/wolfgame.py index 3facd5e..31c88c6 100644 --- a/src/wolfgame.py +++ b/src/wolfgame.py @@ -416,6 +416,7 @@ def reset(): var.START_VOTES = set() # list of players who have voted to !start var.LOVERS = {} # need to be here for purposes of random var.ENTRANCED = set() + var.WILD_CHILDREN = set() reset_settings() @@ -2917,6 +2918,31 @@ def del_player(cli, nick, forced_death=False, devoice=True, end_game=True, death if nickrole == "clone" and nick in var.CLONED: del var.CLONED[nick] + for child in var.ROLES["wild child"].copy(): + if child in var.IDOLS and child not in deadlist: + if var.IDOLS[child] in deadlist: + pm(cli, child, messages["idol_died"]) + var.WILD_CHILDREN.add(child) + var.ROLES["wild child"].remove(child) + var.ROLES["wolf"].add(child) + var.FINAL_ROLES[child] = "wolf" + wolves = var.list_players(var.WOLFCHAT_ROLES) + wolves.remove(child) + mass_privmsg(cli, wolves, messages["wild_child_as_wolf"].format(child)) + if var.PHASE == "day": + random.shuffle(wolves) + for i, wolf in enumerate(wolves): + wolfroles = var.get_role(wolf) + cursed = "" + if wolf in var.ROLES["cursed villager"]: + cursed = "cursed " + wolves[i] = "\u0002{0}\u0002 ({1}{2})".format(wolf, cursed, wolfroles) + + if wolves: + pm(cli, child, "Wolves: " + ", ".join(wolves)) + else: + pm(cli, child, messages["no_other_wolves"]) + if death_triggers and var.PHASE in var.GAME_PHASES: if nick in var.LOVERS: others = var.LOVERS[nick].copy() @@ -3594,7 +3620,7 @@ def rename_player(cli, prefix, nick): if prefix in dictvar.keys(): del dictvar[prefix] 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): + var.DOCTORS, var.BITTEN_ROLES, var.LYCAN_ROLES, var.AMNESIAC_ROLES, var.IDOLS): if prefix in dictvar.keys(): dictvar[nick] = dictvar.pop(prefix) # Looks like {'jacob2': ['5'], '7': ['3']} @@ -3640,7 +3666,7 @@ def rename_player(cli, prefix, nick): var.DISEASED, var.TOBEDISEASED, var.RETRIBUTION, var.MISDIRECTED, var.TOBEMISDIRECTED, var.EXCHANGED, var.IMMUNIZED, var.CURED_LYCANS, var.ALPHA_WOLVES, var.CURSED, var.CHARMERS, var.CHARMED, var.TOBECHARMED, var.PRIESTS, var.CONSECRATING, var.ENTRANCED_DYING, var.DYING, - var.DECEIVED): + var.DECEIVED, var.WILD_CHILDREN): if prefix in setvar: setvar.remove(prefix) setvar.add(nick) @@ -4019,6 +4045,15 @@ def transition_day(cli, gameid=0): choose.func(cli, mm, mm, lovers[0] + " " + lovers[1], sendmsg=False) pm(cli, mm, messages["random_matchmaker"]) + for child in var.ROLES["wild child"]: + if child not in var.IDOLS: + ps = pl[:] + pl.remove(child) + if ps: + target = random.choice(ps) + var.IDOLS[child] = target + pm(cli, child, messages["wild_child_random_idol"].format(target)) + # Reset daytime variables var.INVESTIGATED = set() @@ -4752,8 +4787,8 @@ def chk_nightdone(cli): nightroles.append(p) if var.FIRST_NIGHT: - actedcount += len(var.MATCHMAKERS | var.CLONED.keys()) - nightroles.extend(get_roles("matchmaker", "clone")) + actedcount += len(var.MATCHMAKERS | var.CLONED.keys() | var.IDOLS.keys()) + nightroles.extend(get_roles("matchmaker", "clone", "wild child")) if var.DISEASED_WOLVES: nightroles = [p for p in nightroles if p not in var.list_players(var.WOLF_ROLES - {"wolf cub", "werecrow", "doomsayer"})] @@ -5003,6 +5038,7 @@ def check_exchange(cli, actor, nick): elif actor_role in var.WOLF_ROLES - {"werecrow", "wolf cub", "alpha wolf"}: if actor in var.KILLS: del var.KILLS[actor] + var.WILD_CHILDREN.discard(actor) elif actor_role == "hunter": if actor in var.OTHER_KILLS: del var.OTHER_KILLS[actor] @@ -5024,6 +5060,8 @@ def check_exchange(cli, actor, nick): del var.HVISITED[actor] elif actor_role in ("seer", "oracle", "augur"): var.SEEN.discard(actor) + elif actor_role == "wild child": # would that ever happen? + var.IDOLS[nick] = var.IDOLS.pop(actor) elif actor_role == "hag": if actor in var.LASTHEXED: if var.LASTHEXED[actor] in var.TOBESILENCED and actor in var.HEXED: @@ -5067,6 +5105,7 @@ def check_exchange(cli, actor, nick): elif nick_role in var.WOLF_ROLES - {"werecrow", "wolf cub", "alpha wolf"}: if nick in var.KILLS: del var.KILLS[nick] + var.WILD_CHILDREN.discard(nick) elif nick_role == "hunter": if nick in var.OTHER_KILLS: del var.OTHER_KILLS[nick] @@ -5088,6 +5127,8 @@ def check_exchange(cli, actor, nick): del var.HVISITED[nick] elif nick_role in ("seer", "oracle", "augur"): var.SEEN.discard(nick) + elif nick_role == "wild child": + var.IDOLS[actor] = var.IDOLS.pop(nick) elif nick_role == "hag": if nick in var.LASTHEXED: if var.LASTHEXED[nick] in var.TOBESILENCED and nick in var.HEXED: @@ -5161,6 +5202,8 @@ def check_exchange(cli, actor, nick): if nick_role == "clone": pm(cli, actor, messages["clone_target"].format(nick_target)) + elif nick_role == "wild child": + pm(cli, actor, messages["wild_child_idol"].format(var.IDOLS[actor])) elif nick_role in var.TOTEM_ORDER: if nick_role == "shaman": pm(cli, actor, messages["shaman_totem"].format(nick_totem)) @@ -5203,6 +5246,8 @@ def check_exchange(cli, actor, nick): if actor_role == "clone": pm(cli, nick, messages["clone_target"].format(actor_target)) + elif actor_role == "wild child": + pm(cli, nick, messages["wild_child_idol"].format(var.IDOLS[nick])) elif actor_role in var.TOTEM_ORDER: if actor_role == "shaman": pm(cli, nick, messages["shaman_totem"].format(actor_totem)) @@ -5917,7 +5962,9 @@ def see(cli, nick, chan, rest): victimrole = var.get_role(victim) vrole = victimrole # keep a copy for logging if role == "seer": - if (victimrole in var.SEEN_WOLF and victimrole not in var.SEEN_DEFAULT) or victim in var.ROLES["cursed villager"]: + if victim in var.WILD_CHILDREN: + victimrole = "wild child" + elif (victimrole in var.SEEN_WOLF and victimrole not in var.SEEN_DEFAULT) or victim in var.ROLES["cursed villager"]: victimrole = "wolf" elif victimrole in var.SEEN_DEFAULT: victimrole = var.DEFAULT_ROLE @@ -6214,8 +6261,9 @@ def change_sides(cli, nick, chan, rest, sendmsg=True): debuglog("{0} ({1}) SIDE {2}".format(nick, var.get_role(nick), team)) chk_nightdone(cli) -@cmd("choose", "match", chan=False, pm=True, playing=True, phases=("night",), roles=("matchmaker",)) -def choose(cli, nick, chan, rest, sendmsg=True): +@cmd("choose", chan=False, pm=True, playing=True, phases=("night",), roles=("matchmaker",)) +@cmd("match", chan=False, pm=True, playing=True, phases=("night",), roles=("matchmaker",)) +def choose_lovers(cli, nick, chan, rest, sendmsg=True): """Select two players to fall in love. You may select yourself as one of the lovers.""" if not var.FIRST_NIGHT: return @@ -6484,6 +6532,29 @@ def charm(cli, nick, chan, rest): chk_nightdone(cli) +@cmd("choose", chan=False, pm=True, playing=True, phases=("night",), roles=("wild child",)) +def choose_idol(cli, nick, chan, rest): + """Pick your idol, if they die, you'll become a wolf!""" + if not var.FIRST_NIGHT: + return + if nick in var.IDOLS: + pm(cli, nick, messages["wild_child_already_picked"]) + return + + victim = get_victim(cli, nick, re.split(" +", rest)[0], False) + if not victim: + return + + if nick == victim: + pm(cli, nick, messages["no_target_self"]) + return + + var.IDOLS[nick] = victim + pm(cli, nick, messages["wild_child_success"].format(victim)) + + debuglog("{0} ({1}) IDOLIZE: {2} ({3})".format(nick, var.get_role(nick), victim, var.get_role(victim))) + chk_nightdone(cli) + @hook("featurelist") # For multiple targets with PRIVMSG def getfeatures(cli, nick, *rest): for r in rest: @@ -7152,10 +7223,16 @@ def transition_night(cli): for lycan in var.ROLES["lycan"]: if lycan in var.PLAYERS and not is_user_simple(lycan): - pm(cli, lycan, (messages["lycan_notify"])) + pm(cli, lycan, messages["lycan_notify"]) else: pm(cli, lycan, messages["lycan_simple"]) + for child in var.ROLES["wild child"]: + if child in var.PLAYERS and not is_user_simple(child): + pm(cli, child, messages["child_notify"]) + else: + pm(cli, child, messages["child_simple"]) + for v_ghost, who in var.VENGEFUL_GHOSTS.items(): if who[0] == "!": continue @@ -7579,6 +7656,7 @@ def start(cli, nick, chan, forced = False, restart = ""): var.SICK = set() var.DULLAHAN_TARGETS = {} var.DECEIVED = set() + var.IDOLS = {} var.DEADCHAT_PLAYERS = set() var.SPECTATING_WOLFCHAT = set() @@ -9121,6 +9199,11 @@ if botconfig.DEBUG_MODE or botconfig.ALLOWED_NORMAL_MODE_COMMANDS: special_case.append("need to kill {0}".format(", ".join(var.DULLAHAN_TARGETS[nickname] - var.DEAD))) else: special_case.append("All targets dead") + elif role == "wild child": + if nickname in var.IDOLS: + special_case.append("picked {0} as idol".format(var.IDOLS[nickname])) + else: + special_case.append("no idol picked yet") if nickname not in var.ORIGINAL_ROLES[role] and role not in var.TEMPLATE_RESTRICTIONS: for old_role in var.role_order(): # order doesn't matter here, but oh well if nickname in var.ORIGINAL_ROLES[old_role] and nickname not in var.ROLES[old_role]: