Split piper and convert it to new APIs

Still some unsplit things that are pending other roles being split.
Other gameplay changes regarding piper:

- Ensure that pipers can never be charmed, even in the case of
  misdirection, luck, and exchange totems.
- Allow pipers to change who they are charming during the night.
- Do not share who pipers picked with other pipers (or in revealroles),
  as these are now changeable and pipers are not made aware of each
  other anyway in the role list at night.
This commit is contained in:
skizzerz 2017-11-30 21:42:26 -06:00
parent 50f77439fb
commit 6efbcca3fe
4 changed files with 203 additions and 151 deletions

View File

@ -396,7 +396,6 @@
"already_cursed": "You have already cursed someone tonight.",
"warlock_pass": "You have chosen not to curse anyone tonight.",
"warlock_pass_wolfchat": "\u0002{0}\u0002 has chosen not to curse anyone tonight.",
"already_charmed": "You have already charmed players tonight.",
"piper_pass": "You have chosen not to charm anyone tonight.",
"turncoat_already_turned": "You have changed sides yesterday night, and may not do so again tonight.",
"turncoat_error": "Please specify which team you wish to side with, villagers or wolves.",
@ -422,7 +421,6 @@
"clone_target_success": "You have chosen to clone \u0002{0}\u0002.",
"clone_clone_clone": "Ambiguous command; if you would like to clone someone whose name is or starts with \"clone\", please use \"clone clone clone\".",
"must_charm_multiple": "You must choose two different people.",
"no_charm_self": "You may not charm yourself.",
"targets_already_charmed": "\u0002{0}\u0002 and \u0002{1}\u0002 are already charmed!",
"target_already_charmed": "\u0002{0}\u0002 is already charmed!",
"charm_success": "You have charmed \u0002{0}\u0002.",

View File

@ -15,7 +15,7 @@ def on_exchange(evt, var, actor, target, actor_role, target_role):
return
special = set(get_players(("harlot", "priest", "prophet", "matchmaker",
"doctor", "hag", "sorcerer", "turncoat", "clone", "piper")))
"doctor", "hag", "sorcerer", "turncoat", "clone")))
evt2 = Event("get_special", {"special": special})
evt2.dispatch(var)
pl = set(get_players())
@ -43,7 +43,7 @@ def on_exchange(evt, var, actor, target, actor_role, target_role):
def on_transition_night_end(evt, var):
# init with all roles that haven't been split yet
special = set(get_players(("harlot", "priest", "prophet", "matchmaker",
"doctor", "hag", "sorcerer", "turncoat", "clone", "piper")))
"doctor", "hag", "sorcerer", "turncoat", "clone")))
evt2 = Event("get_special", {"special": special})
evt2.dispatch(var)
pl = set(get_players())

193
src/roles/piper.py Normal file
View File

@ -0,0 +1,193 @@
import re
import random
import itertools
import math
from collections import defaultdict
import botconfig
import src.settings as var
from src.utilities import *
from src import channels, users, debuglog, errlog, plog
from src.decorators import command, event_listener
from src.messages import messages
from src.events import Event
TOBECHARMED = {} # type: Dict[users.User, Set[users.User]]
CHARMED = set() # type: Set[users.User]
@command("charm", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("piper",))
def charm(var, wrapper, message):
"""Charm a player, slowly leading to your win!"""
pieces = re.split(" +", message)
target1 = pieces[0]
if len(pieces) > 1:
if len(pieces) > 2 and pieces[1].lower() == "and":
target2 = pieces[2]
else:
target2 = pieces[1]
else:
target2 = None
target1 = get_target(var, wrapper, target1)
if not target1:
return
if target2 is not None:
target2 = get_target(var, wrapper, target2)
if not target2:
return
orig1 = target1
orig2 = target2
evt1 = Event("targeted_command", {"target": target1.nick, "misdirection": True, "exchange": True})
evt1.dispatch(wrapper.client, var, "charm", wrapper.source.nick, target1.nick, frozenset({"detrimental"}))
if evt1.prevent_default:
return
target1 = users._get(evt.data["target"]) # FIXME: need to make targeted_command use users
if target2 is not None:
evt2 = Event("targeted_command", {"target": target2.nick, "misdirection": True, "exchange": True})
evt2.dispatch(wrapper.client, var, "charm", wrapper.source.nick, target2.nick, frozenset({"detrimental"}))
if evt2.prevent_default:
return
target2 = users._get(evt.data["target"]) # FIXME
# Do these checks based on original targets, so piper doesn't know to change due to misdirection/luck totem
if orig1 is orig2:
wrapper.pm(messages["must_charm_multiple"])
return
if orig1 in CHARMED and orig2 in CHARMED:
wrapper.pm(messages["targets_already_charmed"].format(orig1, orig2))
return
elif orig1 in CHARMED:
wrapper.pm(messages["target_already_charmed"].format(orig1))
return
elif orig2 in CHARMED:
wrapper.pm(messages["target_already_charmed"].format(orig2))
return
TOBECHARMED[wrapper.source] = set(target1, target2)
TOBECHARMED[wrapper.source].discard(None)
if orig2:
debuglog("{0} (piper) CHARM {1} ({2}) && {3} ({4})".format(wrapper.source,
target1, get_main_role(target1),
target2, get_main_role(target2)))
wrapper.pm(messages["charm_multiple_success"].format(orig1, ori2))
else:
debuglog("{0} (piper) CHARM {1} ({2})".format(wrapper.source, target1, get_main_role(target1)))
wrapper.pm(messages["charm_success"].format(orig1))
chk_nightdone(wrapper.client)
@event_listener("chk_win", priority=2)
def on_chk_win(evt, cli, var, rolemap, mainroles, lpl, lwolves, lrealwolves):
lp = len(rolemap.get("piper", ()))
# lpl doesn't included wounded/sick people or consecrating priests
# whereas we want to ensure EVERYONE (even wounded people) are charmed for piper win
if var.PHASE == "day" and lp + len(CHARMED) == len(get_players()):
evt.data["winner"] = "pipers"
evt.data["message"] = messages["piper_win"].format("s" if lp > 1 else "", "s" if lp == 1 else "")
@event_listener("player_win")
def on_player_win(evt, var, player, mainrole, winner, survived):
if winner != "pipers":
return
if mainrole == "piper":
evt.data["won"] = True
# TODO: add code here (or maybe a sep event?) to let lovers win alongside piper
# Right now that's still in wolfgame.py, but should be moved here once mm is split
@event_listener("del_player")
def on_del_player(evt, var, player, mainrole, allroles, death_triggers):
CHARMED.discard(player)
TOBECHARMED.pop(player, None)
@event_listener("transition_day_begin")
def on_transition_day_begin(evt, var):
tocharm = set(itertools.chain(TOBECHARMED.values()))
# remove pipers from set; they can never be charmed
# but might end up in there due to misdirection/luck totems
tocharm.difference_update(get_all_players(("piper",)))
# Send out PMs to players who have been charmed
for target in tocharm:
charmedlist = list(CHARMED | tocharm - {victim})
message = messages["charmed"]
if len(charmedlist) <= 0:
target.pm(message + messages["no_charmed_players"])
elif len(charmedlist) == 1:
target.pm(message + messages["one_charmed_player"].format(charmedlist[0]))
elif len(charmedlist) == 2:
target.pm(message + messages["two_charmed_players"].format(charmedlist[0], charmedlist[1]))
else:
target.pm(message + messages["many_charmed_players"].format("\u0002, \u0002".join(charmedlist[:-1]), charmedlist[-1]))
for target in CHARMED:
tobecharmedlist = list(tocharm)
if len(tobecharmedlist) == 1:
message = messages["players_charmed_one"].format(tobecharmedlist[0])
elif len(tobecharmedlist) == 2:
message = messages["players_charmed_two"].format(tobecharmedlist[0], tobecharmedlist[1])
else:
message = messages["players_charmed_many"].format("\u0002, \u0002".join(tobecharmedlist[:-1]), tobecharmedlist[-1])
previouscharmed = CHARMED - {target}
if len(previouscharmed):
target.pm(message + messages["previously_charmed"].format("\u0002, \u0002".join(previouscharmed)))
else:
target.pm(message)
CHARMED.update(tocharm)
TOBECHARMED.clear()
@event_listener("night_acted")
def on_night_acted(evt, var, target, actor):
if target in TOBECHARMED:
evt.data["acted"] = True
@event_listener("chk_nightdone")
def on_chk_nightdone(evt, var):
evt.data["actedcount"] += len(TOBECHARMED.keys())
evt.data["nightroles"].extend(get_all_players(("piper",)))
@event_listener("transition_night_end", priority=2)
def on_transition_night_end(evt, var):
ps = set(get_players()) - CHARMED
for piper in get_all_players(("piper",)):
pl = ps[:]
random.shuffle(pl)
pl.remove(piper)
to_send = "piper_notify"
if piper.prefers_simple():
to_send = "piper_simple"
piper.send(messages[to_send], "Players: " + ", ".join(pl), sep="\n")
@event_listener("exchange_roles")
def on_exchange(evt, var, actor, target, actor_role, target_role):
# if we're shifting piper around, ensure that the new piper isn't charmed
if actor_role == "piper":
CHARMED.discard(target)
if target_role == "piper":
CHARMED.discard(actor)
@event_listener("get_special")
def on_get_special(evt, var):
evt.data["special"].extend(get_players(("piper",)))
@event_listener("reset")
def on_reset(evt, var):
CHARMED.clear()
TOBECHARMED.clear()
@event_listener("revealroles")
def on_revealroles(evt, var, wrapper):
if CHARMED:
evt.data["output"].append("\u0002charmed players\u0002: {0}".format(", ".join(CHARMED)))
# vim: set sw=4 expandtab:

View File

@ -98,7 +98,6 @@ var.RESTARTING = False
var.BITTEN_ROLES = {}
var.LYCAN_ROLES = {}
var.CHARMED = set()
var.START_VOTES = set()
if botconfig.DEBUG_MODE and var.DISABLE_DEBUG_MODE_TIMERS:
@ -2132,7 +2131,7 @@ def stop_game(winner="", abort=False, additional_winners=None, log=True):
# determine if this player's team won
if rol in var.TRUE_NEUTRAL_ROLES:
# most true neutral roles never have a team win, only individual wins. Exceptions to that are here
teams = {"monster":"monsters", "piper":"pipers", "demoniac":"demoniacs"}
teams = {"monster":"monsters", "demoniac":"demoniacs"}
if rol in teams and winner == teams[rol]:
won = True
elif rol == "turncoat" and splr in var.TURNCOATS and var.TURNCOATS[splr][0] != "none":
@ -2168,6 +2167,7 @@ def stop_game(winner="", abort=False, additional_winners=None, log=True):
elif winner == "demoniacs" and lvrrol == "demoniac":
iwon = True
break
# TODO: split this into piper.py once matchmaker is split
elif winner == "pipers" and lvrrol == "piper":
iwon = True
break
@ -2175,8 +2175,6 @@ def stop_game(winner="", abort=False, additional_winners=None, log=True):
iwon = True
elif rol == "demoniac" and splr in survived and winner == "demoniacs":
iwon = True
elif rol == "piper" and splr in survived and winner == "pipers":
iwon = True
elif rol == "clone":
# this means they ended game while being clone and not some other role
if splr in survived and not winner.startswith("@") and singular(winner) not in var.WIN_STEALER_ROLES:
@ -2311,7 +2309,6 @@ def chk_win_conditions(cli, rolemap, mainroles, end_game=True, winner=None):
lmonsters = len(rolemap.get("monster", ()))
ldemoniacs = len(rolemap.get("demoniac", ()))
ltraitors = len(rolemap.get("traitor", ()))
lpipers = len(rolemap.get("piper", ()))
message = ""
# fool won, chk_win was called from !lynch
@ -2321,9 +2318,6 @@ def chk_win_conditions(cli, rolemap, mainroles, end_game=True, winner=None):
message = messages["no_win"]
# still want people like jesters, dullahans, etc. to get wins if they fulfilled their win conds
winner = "no_team_wins"
elif var.PHASE == "day" and lpipers and len(list_players()) - lpipers == len(var.CHARMED - var.ROLES["piper"]):
winner = "pipers"
message = messages["piper_win"].format("s" if lpipers > 1 else "", "s" if lpipers == 1 else "")
elif lrealwolves == 0 and ltraitors == 0 and lcubs == 0:
if ldemoniacs > 0:
s = "s" if ldemoniacs > 1 else ""
@ -2344,6 +2338,7 @@ def chk_win_conditions(cli, rolemap, mainroles, end_game=True, winner=None):
message = messages["monster_wolf_win"].format(s)
winner = "monsters"
# TODO: convert to using users, flip priority order (so that things like fool run last, and therefore override previous win conds)
# Priorities:
# 0 = fool, other roles that end game immediately
# 1 = things that could short-circuit game ending, such as cub growing up or traitor turning
@ -2403,8 +2398,6 @@ def del_player(player, *, devoice=True, end_game=True, death_triggers=True, kill
var.ROLES[r].remove(player.nick) # FIXME
if player.nick in var.BITTEN_ROLES:
del var.BITTEN_ROLES[player.nick] # FIXME
if player.nick in var.CHARMED:
var.CHARMED.remove(player.nick) # FIXME
pl.discard(player)
# handle roles that trigger on death
# clone happens regardless of death_triggers being true or not
@ -2681,7 +2674,7 @@ def del_player(player, *, devoice=True, end_game=True, death_triggers=True, kill
# remove players from night variables
# the dicts are handled above, these are the lists of who has acted which is used to determine whether night should end
# if these aren't cleared properly night may end prematurely
for x in (var.PASSED, var.HEXED, var.MATCHMAKERS, var.CURSED, var.CHARMERS):
for x in (var.PASSED, var.HEXED, var.MATCHMAKERS, var.CURSED):
x.discard(player.nick)
if var.PHASE == "day" and ret:
var.VOTES.pop(player.nick, None) # Delete other people's votes on the player
@ -3046,8 +3039,7 @@ def rename_player(var, user, prefix):
for setvar in (var.HEXED, var.SILENCED, var.MATCHMAKERS, var.PASSED,
var.JESTERS, var.AMNESIACS, var.LYCANTHROPES, var.LUCKY, var.DISEASED,
var.MISDIRECTED, var.EXCHANGED, var.IMMUNIZED, var.CURED_LYCANS,
var.ALPHA_WOLVES, var.CURSED, var.CHARMERS, var.CHARMED, var.TOBECHARMED,
var.PRIESTS, var.CONSECRATING):
var.ALPHA_WOLVES, var.CURSED, var.PRIESTS, var.CONSECRATING):
if prefix in setvar:
setvar.remove(prefix)
setvar.add(nick)
@ -3404,40 +3396,6 @@ def transition_day(cli, gameid=0):
var.WOUNDED = set()
var.NO_LYNCH = set()
# Send out PMs to players who have been charmed
for victim in var.TOBECHARMED:
charmedlist = list(var.CHARMED | var.TOBECHARMED - {victim})
message = messages["charmed"]
if len(charmedlist) <= 0:
pm(cli, victim, message + messages["no_charmed_players"])
elif len(charmedlist) == 1:
pm(cli, victim, message + messages["one_charmed_player"].format(charmedlist[0]))
elif len(charmedlist) == 2:
pm(cli, victim, message + messages["two_charmed_players"].format(charmedlist[0], charmedlist[1]))
else:
pm(cli, victim, message + messages["many_charmed_players"].format("\u0002, \u0002".join(charmedlist[:-1]), charmedlist[-1]))
if var.TOBECHARMED:
tobecharmedlist = list(var.TOBECHARMED)
for victim in var.CHARMED:
if len(tobecharmedlist) == 1:
message = messages["players_charmed_one"].format(tobecharmedlist[0])
elif len(tobecharmedlist) == 2:
message = messages["players_charmed_two"].format(tobecharmedlist[0], tobecharmedlist[1])
else:
message = messages["players_charmed_many"].format(
"\u0002, \u0002".join(tobecharmedlist[:-1]), tobecharmedlist[-1])
previouscharmed = var.CHARMED - {victim}
if len(previouscharmed):
pm(cli, victim, message + (messages["previously_charmed"]).format("\u0002, \u0002".join(previouscharmed)))
else:
pm(cli, victim, message)
var.CHARMED.update(var.TOBECHARMED)
var.TOBECHARMED.clear()
for crow, target in iter(var.OBSERVED.items()):
if crow not in var.ROLES["werecrow"]:
continue
@ -3445,7 +3403,7 @@ def transition_day(cli, gameid=0):
user = users._get(target) # FIXME
evt = Event("night_acted", {"acted": False})
evt.dispatch(var, user, actor)
if ((target in var.PRAYED and var.PRAYED[target][0] > 0) or target in var.CHARMERS or
if ((target in var.PRAYED and var.PRAYED[target][0] > 0) or
target in var.OBSERVED or target in var.HEXED or target in var.CURSED or evt.data["acted"]):
actor.send(messages["werecrow_success"].format(user))
else:
@ -3846,10 +3804,9 @@ def chk_nightdone(cli):
pl = get_players()
spl = set(pl)
actedcount = sum(map(len, (var.PASSED, var.OBSERVED,
var.HEXED, var.CURSED, var.CHARMERS)))
actedcount = sum(map(len, (var.PASSED, var.OBSERVED, var.HEXED, var.CURSED)))
nightroles = list(get_all_players(("sorcerer", "hag", "warlock", "werecrow", "piper", "prophet")))
nightroles = list(get_all_players(("sorcerer", "hag", "warlock", "werecrow", "prophet")))
for nick, info in var.PRAYED.items():
if info[0] > 0:
@ -4911,81 +4868,6 @@ def clone(cli, nick, chan, rest):
var.ROLE_COMMAND_EXCEPTIONS.add("clone")
@cmd("charm", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("piper",))
def charm(cli, nick, chan, rest):
"""Charm a player, slowly leading to your win!"""
if nick in var.CHARMERS:
pm(cli, nick, messages["already_charmed"])
return
pieces = re.split(" +",rest)
victim = pieces[0]
if len(pieces) > 1:
if len(pieces) > 2 and pieces[1].lower() == "and":
victim2 = pieces[2]
else:
victim2 = pieces[1]
else:
victim2 = None
victim = get_victim(cli, nick, victim, False, True)
if not victim:
return
if is_safe(nick, victim):
pm(cli, nick, messages["no_acting_on_succubus"].format("charm"))
return
if victim2 is not None:
victim2 = get_victim(cli, nick, victim2, False, True)
if not victim2:
return
if is_safe(nick, victim2):
pm(cli, nick, messages["no_acting_on_succubus"].format("charm"))
return
if victim == victim2:
pm(cli, nick, messages["must_charm_multiple"])
return
if nick in (victim, victim2):
pm(cli, nick, messages["no_charm_self"])
return
charmedlist = var.CHARMED|var.TOBECHARMED
if victim in charmedlist or victim2 and victim2 in charmedlist:
if victim in charmedlist and victim2 and victim2 in charmedlist:
pm(cli, nick, messages["targets_already_charmed"].format(victim, victim2))
return
if (len(list_players()) - len(var.ROLES["piper"]) - len(charmedlist) - 2 >= 0 or
victim in charmedlist and not victim2):
pm(cli, nick, messages["target_already_charmed"].format(victim in charmedlist and victim or victim2))
return
var.CHARMERS.add(nick)
var.PASSED.discard(nick)
var.TOBECHARMED.add(victim)
if victim2:
var.TOBECHARMED.add(victim2)
pm(cli, nick, messages["charm_multiple_success"].format(victim, victim2))
else:
pm(cli, nick, messages["charm_success"].format(victim))
# if there are other pipers, tell them who gets charmed (so they don't have to keep guessing who they are still allowed to charm)
for piper in var.ROLES["piper"]:
if piper != nick:
if victim2:
pm(cli, piper, messages["another_piper_charmed_multiple"].format(victim, victim2))
else:
pm(cli, piper, messages["another_piper_charmed"].format(victim))
if victim2:
debuglog("{0} ({1}) CHARM {2} ({3}) && {4} ({5})".format(nick, get_role(nick),
victim, get_role(victim),
victim2, get_role(victim2)))
else:
debuglog("{0} ({1}) CHARM {2} ({3})".format(nick, get_role(nick),
victim, get_role(victim)))
chk_nightdone(cli)
@event_listener("targeted_command", priority=9)
def on_targeted_command(evt, cli, var, cmd, actor, orig_target, tags):
if evt.data["misdirection"]:
@ -5163,7 +5045,6 @@ def transition_night(cli):
var.CURSED = set() # set of warlocks that have cursed
var.PASSED = set()
var.OBSERVED = {} # those whom werecrows have observed
var.CHARMERS = set() # pipers who have charmed
var.TOBESILENCED = set()
var.CONSECRATING = set()
for nick in var.PRAYED:
@ -5323,19 +5204,6 @@ def transition_night(cli):
pm(cli, ass, messages["assassin_simple"])
pm(cli, ass, "Players: " + ", ".join(pl))
for piper in var.ROLES["piper"]:
pl = ps[:]
random.shuffle(pl)
pl.remove(piper)
for charmed in var.CHARMED:
if charmed in pl: # corner case: if there are multiple pipers and a piper is charmed, the piper will be in var.CHARMED but not in pl
pl.remove(charmed)
if piper in var.PLAYERS and not is_user_simple(piper):
pm(cli, piper, (messages["piper_notify"]))
else:
pm(cli, piper, messages["piper_simple"])
pm(cli, piper, "Players: " + ", ".join(pl))
for turncoat in var.ROLES["turncoat"]:
# they start out as unsided, but can change n1
if turncoat not in var.TURNCOATS:
@ -5654,9 +5522,6 @@ def start(cli, nick, chan, forced = False, restart = ""):
var.BITTEN_ROLES = {}
var.LYCAN_ROLES = {}
var.AMNESIAC_ROLES = {}
var.CHARMERS = set()
var.CHARMED = set()
var.TOBECHARMED = set()
var.ACTIVE_PROTECTIONS = defaultdict(list)
var.TURNCOATS = {}
var.EXCHANGED_ROLES = []
@ -7015,10 +6880,6 @@ def revealroles(var, wrapper, message):
if var.IMMUNIZED:
output.append("\u0002immunized\u0002: {0}".format(", ".join(var.IMMUNIZED)))
# get charmed players
if var.CHARMED | var.TOBECHARMED:
output.append("\u0002charmed players\u0002: {0}".format(", ".join(var.CHARMED | var.TOBECHARMED)))
evt = Event("revealroles", {"output": output})
evt.dispatch(var, wrapper)