Amnesiac fixes and redo stop_game readout logic

- Account for revealing totem + amnesiac in experimental !stats properly
- Fix amnesiac blacklist checks to be consistent with each other
- Remove non-events from villager.py -- these always ran before or after
  all other events, so there was no point in them being events in the
  first place
- stop_game now follows the mainroles/allroles pattern instead of
  roles/templates pattern. This also modifies the data stored in db
  stats, and fixes readouts for cases where we do goofy stuff with
  secondary roles
This commit is contained in:
skizzerz 2018-04-23 23:11:02 -05:00
parent 1a446205ce
commit 56f2bacd3a
6 changed files with 105 additions and 100 deletions

View File

@ -713,6 +713,8 @@
"guardian_villager_win": "Game over! All the wolves are dead! The remaining villagers throw a party in honor of the guardian angels that watched over the village, and live happily ever after.", "guardian_villager_win": "Game over! All the wolves are dead! The remaining villagers throw a party in honor of the guardian angels that watched over the village, and live happily ever after.",
"guardian_lose_no_guards": "Game over! The remaining villagers managed to destroy the wolves, however the guardians that used to watch over the village are nowhere to be found. The village lives on in an uneasy peace, not knowing when they will be destroyed completely now that they are defenseless.", "guardian_lose_no_guards": "Game over! The remaining villagers managed to destroy the wolves, however the guardians that used to watch over the village are nowhere to be found. The village lives on in an uneasy peace, not knowing when they will be destroyed completely now that they are defenseless.",
"guardian_lose_with_guards": "Game over! The guardians, angered by the loss of everyone they were meant to guard, engage the wolves in battle. After the dust settles, the wolves remain standing.", "guardian_lose_with_guards": "Game over! The guardians, angered by the loss of everyone they were meant to guard, engage the wolves in battle. After the dust settles, the wolves remain standing.",
"endgame_roleswap_long": "was {0}",
"endgame_roleswap_short": "{0}",
"sleepy_nightmare_begin": "While walking through the woods, you hear the clopping of hooves behind you. Turning around, you see a large black horse with dark red eyes and flames where its mane and tail would be. After a brief period of time, it starts chasing after you! You think if you can cross the bridge over the nearby river you'll be safe, but your surroundings are almost unrecognizable in this darkness.", "sleepy_nightmare_begin": "While walking through the woods, you hear the clopping of hooves behind you. Turning around, you see a large black horse with dark red eyes and flames where its mane and tail would be. After a brief period of time, it starts chasing after you! You think if you can cross the bridge over the nearby river you'll be safe, but your surroundings are almost unrecognizable in this darkness.",
"sleepy_nightmare_navigate": "You can pm me \"north\", \"east\", \"south\", and \"west\", or their abbreviations \"n\", \"e\", \"s\", and \"w\" to navigate.", "sleepy_nightmare_navigate": "You can pm me \"north\", \"east\", \"south\", and \"west\", or their abbreviations \"n\", \"e\", \"s\", and \"w\" to navigate.",
"sleepy_nightmare_0": "You find yourself deep in the heart of the woods, with imposing trees covering up what little light exists with their dense canopy. The paths here are very twisty, and it's easy to wind up going in circles if one is not careful. Directions are {0}.", "sleepy_nightmare_0": "You find yourself deep in the heart of the woods, with imposing trees covering up what little light exists with their dense canopy. The paths here are very twisty, and it's easy to wind up going in circles if one is not careful. Directions are {0}.",

View File

@ -260,12 +260,15 @@ def add_game(mode, size, started, finished, winner, players, options):
Players dict format: Players dict format:
{ {
version: 2 (key is omitted for v1)
nick: "Nickname" nick: "Nickname"
account: "Account name" (or None, "*" is converted to None) account: "Account name" (or None, "*" is converted to None)
ident: "Ident" ident: "Ident"
host: "Host" host: "Host"
role: "role name" mainrole: "role name" (v2+)
templates: ["template names", ...] allroles: {"role name", ...} (v2+)
role: "role name" (v1 only)
templates: ["template names", ...] (v1 only)
special: ["special qualities", ... (lover, entranced, etc.)] special: ["special qualities", ... (lover, entranced, etc.)]
won: True/False won: True/False
iwon: True/False iwon: True/False

View File

@ -14,25 +14,35 @@ from src.messages import messages
from src.events import Event from src.events import Event
ROLES = UserDict() # type: Dict[users.User, str] ROLES = UserDict() # type: Dict[users.User, str]
STATS_FLAG = False # if True, we begin accounting for amnesiac in update_stats
def _get_blacklist(var):
# matchmaker is blacklisted if AMNESIAC_NIGHTS > 1 due to only being able to act night 1.
@event_listener("role_assignment")
def on_role_assignment(evt, var, gamemode, pl):
# 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. # 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 # 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 # 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) # nontrivial and will likely require a great deal of thought (and likely new tracking vars)
# FIXME: once experimental stats become the new stats, clone and traitor will work properly
# and we can remove those from hardcoded blacklist and remove this comment block.
blacklist = var.TEMPLATE_RESTRICTIONS.keys() | var.AMNESIAC_BLACKLIST | {var.DEFAULT_ROLE, "amnesiac", "clone", "traitor"}
if var.AMNESIAC_NIGHTS > 1:
blacklist.add("matchmaker")
return blacklist
roles = var.ROLE_GUIDE.keys() - var.TEMPLATE_RESTRICTIONS.keys() - var.AMNESIAC_BLACKLIST - {var.DEFAULT_ROLE, "amnesiac", "clone", "traitor"} @event_listener("role_assignment")
if var.AMNESIAC_NIGHTS > 1 and "matchmaker" in roles: def on_role_assignment(evt, var, gamemode, pl):
roles.remove("matchmaker") roles = var.ROLE_GUIDE.keys() - _get_blacklist(var)
for amnesiac in get_all_players(("amnesiac",)): for amnesiac in get_all_players(("amnesiac",)):
ROLES[amnesiac] = random.choice(list(roles)) ROLES[amnesiac] = random.choice(list(roles))
@event_listener("transition_night_begin") @event_listener("transition_night_begin")
def on_transition_night_begin(evt, var): def on_transition_night_begin(evt, var):
global STATS_FLAG
if var.NIGHT_COUNT == var.AMNESIAC_NIGHTS: if var.NIGHT_COUNT == var.AMNESIAC_NIGHTS:
amnesiacs = get_all_players(("amnesiac",)) amnesiacs = get_all_players(("amnesiac",))
if amnesiacs and not var.HIDDEN_AMNESIAC:
STATS_FLAG = True
for amn in amnesiacs: for amn in amnesiacs:
role = ROLES[amn] role = ROLES[amn]
@ -72,6 +82,7 @@ def on_investigate(evt, var, actor, target):
@event_listener("exchange_roles") @event_listener("exchange_roles")
def on_exchange_roles(evt, var, actor, target, actor_role, target_role): def on_exchange_roles(evt, var, actor, target, actor_role, target_role):
# FIXME: exchange totem messes with var.HIDDEN_AMNESIAC (the new amnesiac is no longer hidden should they die)
if actor_role == "amnesiac": if actor_role == "amnesiac":
actor_role = ROLES[actor] actor_role = ROLES[actor]
if target in ROLES: if target in ROLES:
@ -94,6 +105,9 @@ def on_exchange_roles(evt, var, actor, target, actor_role, target_role):
@event_listener("revealing_totem") @event_listener("revealing_totem")
def on_revealing_totem(evt, var, votee): def on_revealing_totem(evt, var, votee):
if evt.data["role"] not in _get_blacklist(var) and not var.HIDDEN_AMNESIAC and len(var.ORIGINAL_ROLES["amnesiac"]):
global STATS_FLAG
STATS_FLAG = True
if evt.data["role"] == "amnesiac": if evt.data["role"] == "amnesiac":
role = ROLES[votee] role = ROLES[votee]
change_role(votee, "amnesiac", role) change_role(votee, "amnesiac", role)
@ -106,15 +120,14 @@ def on_revealing_totem(evt, var, votee):
@event_listener("get_reveal_role") @event_listener("get_reveal_role")
def on_reveal_role(evt, var, user): def on_reveal_role(evt, var, user):
if var.HIDDEN_AMNESIAC and user in var.ORIGINAL_ROLES["amnesiac"]: if var.HIDDEN_AMNESIAC and var.ORIGINAL_MAIN_ROLES[user] == "amnesiac":
evt.data["role"] = "amnesiac" evt.data["role"] = "amnesiac"
@event_listener("get_endgame_message") @event_listener("get_endgame_message")
def on_get_endgame_message(evt, var, role, players, original_roles): def on_get_endgame_message(evt, var, player, role, is_mainrole):
if role == "amnesiac": if role == "amnesiac":
for player in players: # FIXME: Harcoded English
evt.data["message"].append("\u0002{0}\u0002 (would be {1})".format(player, ROLES[player])) evt.data["message"].append("would be {0}".format(ROLES[player]))
evt.stop_processing = True
@event_listener("revealroles_role") @event_listener("revealroles_role")
def on_revealroles_role(evt, var, user, role): def on_revealroles_role(evt, var, user, role):
@ -123,12 +136,13 @@ def on_revealroles_role(evt, var, user, role):
@event_listener("update_stats") @event_listener("update_stats")
def on_update_stats(evt, var, player, mainrole, revealrole, allroles): def on_update_stats(evt, var, player, mainrole, revealrole, allroles):
if not var.HIDDEN_AMNESIAC and var.NIGHT_COUNT >= var.AMNESIAC_NIGHTS: if STATS_FLAG and not _get_blacklist(var) & {mainrole, revealrole}:
if not var.AMNESIAC_BLACKLIST & {mainrole, revealrole}: # make sure roles aren't blacklisted evt.data["possible"].add("amnesiac")
evt.data["possible"].add("amnesiac")
@event_listener("reset") @event_listener("reset")
def on_reset(evt, var): def on_reset(evt, var):
global STATS_FLAG
ROLES.clear() ROLES.clear()
STATS_FLAG = False
# vim: set sw=4 expandtab: # vim: set sw=4 expandtab:

View File

@ -35,25 +35,6 @@ def on_transition_night_end(evt, var):
cultist.queue_message(messages[to_send]) cultist.queue_message(messages[to_send])
cultist.send_messages() cultist.send_messages()
# No listeners should register before this one
# This sets up the initial state, based on village/wolfteam/neutral affiliation
@event_listener("player_win", priority=0)
def on_player_win(evt, var, user, role, winner, survived):
# init won/iwon to False
evt.data["won"] = False
evt.data["iwon"] = False
if role in var.WOLFTEAM_ROLES or (var.DEFAULT_ROLE == "cultist" and role in var.HIDDEN_ROLES):
if winner == "wolves":
evt.data["won"] = True
evt.data["iwon"] = survived
elif role in var.TRUE_NEUTRAL_ROLES:
# handled in their individual files
pass
elif winner == "villagers":
evt.data["won"] = True
evt.data["iwon"] = survived
@event_listener("chk_win", priority=3) @event_listener("chk_win", priority=3)
def on_chk_win(evt, var, rolemap, mainroles, lpl, lwolves, lrealwolves): def on_chk_win(evt, var, rolemap, mainroles, lpl, lwolves, lrealwolves):
if evt.data["winner"] is not None: if evt.data["winner"] is not None:
@ -68,18 +49,4 @@ def on_chk_win(evt, var, rolemap, mainroles, lpl, lwolves, lrealwolves):
evt.data["winner"] = "wolves" evt.data["winner"] = "wolves"
evt.data["message"] = messages["wolf_win_greater"] evt.data["message"] = messages["wolf_win_greater"]
@event_listener("get_final_role", priority=0)
def on_get_final_role(evt, var, user, role):
if user.nick in var.FINAL_ROLES:
evt.data["role"] = var.FINAL_ROLES[user.nick]
@event_listener("get_endgame_message", priority=10)
def on_get_endgame_message(evt, var, role, players, original_roles):
for player in players:
if player in original_roles and role not in var.TEMPLATE_RESTRICTIONS:
evt.data["message"].append("\u0002{0}\u0002 ({1}{2})".format(player, "" if evt.data["done"] else "was ", original_roles[player]))
evt.data["done"] = True
else:
evt.data["message"].append("\u0002{0}\u0002".format(player))
# vim: set sw=4 expandtab: # vim: set sw=4 expandtab:

View File

@ -186,7 +186,7 @@ class User(IRCContext):
self._ident = ident self._ident = ident
self._host = host self._host = host
self.realname = realname self.realname = realname
self.account = account if not var.DISABLE_ACCOUNTS else None self.account = account
self.channels = {} self.channels = {}
self.timestamp = time.time() self.timestamp = time.time()
self.sets = [] self.sets = []
@ -199,7 +199,7 @@ class User(IRCContext):
self.ident = ident self.ident = ident
self.host = host self.host = host
self.realname = realname self.realname = realname
self.account = account if not var.DISABLE_ACCOUNTS else None self.account = account
self.timestamp = time.time() self.timestamp = time.time()
elif ident is not None and host is not None: elif ident is not None and host is not None:

View File

@ -73,7 +73,6 @@ var.LAST_GOAT = {}
var.USERS = {} var.USERS = {}
var.ADMIN_PINGING = False var.ADMIN_PINGING = False
var.ORIGINAL_ROLES = UserDict() # type: Dict[str, Set[users.User]]
var.DCED_LOSERS = UserSet() # type: Set[users.User] var.DCED_LOSERS = UserSet() # type: Set[users.User]
var.PLAYERS = {} var.PLAYERS = {}
var.DCED_PLAYERS = {} var.DCED_PLAYERS = {}
@ -84,7 +83,9 @@ var.TIMERS = {}
var.OLD_MODES = defaultdict(set) var.OLD_MODES = defaultdict(set)
var.ROLES = UserDict() # type: Dict[str, Set[users.User]] var.ROLES = UserDict() # type: Dict[str, Set[users.User]]
var.ORIGINAL_ROLES = UserDict() # type: Dict[str, Set[users.User]]
var.MAIN_ROLES = UserDict() # type: Dict[users.User, str] var.MAIN_ROLES = UserDict() # type: Dict[users.User, str]
var.ORIGINAL_MAIN_ROLES = UserDict() # type: Dict[users.User, str]
var.ALL_PLAYERS = UserList() var.ALL_PLAYERS = UserList()
var.FORCE_ROLES = DefaultUserDict(UserSet) var.FORCE_ROLES = DefaultUserDict(UserSet)
@ -340,6 +341,7 @@ def reset():
var.ORIGINAL_ROLES.clear() var.ORIGINAL_ROLES.clear()
var.ROLES["person"] = UserSet() var.ROLES["person"] = UserSet()
var.MAIN_ROLES.clear() var.MAIN_ROLES.clear()
var.ORIGINAL_MAIN_ROLES.clear()
var.FORCE_ROLES.clear() var.FORCE_ROLES.clear()
evt = Event("reset", {}) evt = Event("reset", {})
@ -1988,36 +1990,53 @@ def stop_game(var, winner="", abort=False, additional_winners=None, log=True):
roles_msg = [] roles_msg = []
origroles = {} # user-based list of original roles # squirrel away a copy of our original roleset for stats recording, as the following code
with copy.deepcopy(var.ORIGINAL_ROLES) as rolelist: # modifies var.ORIGINAL_ROLES and var.ORIGINAL_MAIN_ROLES.
for role, playerlist in var.ORIGINAL_ROLES.items(): rolecounts = {role: len(players) for role, players in var.ORIGINAL_ROLES.items()}
if role in var.TEMPLATE_RESTRICTIONS.keys():
continue
for p in playerlist:
# The final role is set at priority 0, other roles can override that
evt = Event("get_final_role", {"role": role})
evt.dispatch(var, p, role)
if role != evt.data["role"]:
origroles[p] = role
rolelist[role].remove(p)
rolelist[evt.data["role"]].add(p)
done = False # save some typing
for role in role_order(): rolemap = var.ORIGINAL_ROLES
if len(rolelist[role]) == 0: mainroles = var.ORIGINAL_MAIN_ROLES
continue orig_main = {} # if get_final_role changes mainroles, we want to stash original main role
evt = Event("get_endgame_message", {"message": [], "done": done})
evt.dispatch(var, role, rolelist[role], origroles)
msg = evt.data["message"] for player, role in mainroles.items():
done = evt.data["done"] evt = Event("get_final_role", {"role": var.FINAL_ROLES.get(player.nick, role)})
evt.dispatch(var, player, role)
if role != evt.data["role"]:
rolemap[role].remove(player)
rolemap[evt.data["role"]].add(player)
mainroles[player] = evt.data["role"]
orig_main[player] = role
if len(rolelist[role]) == 2: # track if we already printed "was" for a role swap, e.g. The wolves were A (was seer), B (harlot)
roles_msg.append("The {1} were {0[0]} and {0[1]}.".format(msg, plural(role))) # so that we can make the message a bit more concise
elif len(rolelist[role]) == 1: roleswap_key = "endgame_roleswap_long"
roles_msg.append("The {1} was {0[0]}.".format(msg, role))
for role in role_order():
numrole = len(rolemap[role])
if numrole == 0:
continue
msg = []
for player in rolemap[role]:
# check if the player changed roles during game, and if so insert the "was X" message
player_msg = []
if mainroles[player] == role and player in orig_main:
player_msg.append(messages[roleswap_key].format(orig_main[player]))
roleswap_key = "endgame_roleswap_short"
evt = Event("get_endgame_message", {"message": player_msg})
evt.dispatch(var, player, role, is_mainrole=mainroles[player] == role)
if player_msg:
msg.append("\u0002{0}\u0002 ({1})".format(player, ", ".join(player_msg)))
else: else:
roles_msg.append("The {2} were {0}, and {1}.".format(", ".join(msg[0:-1]), msg[-1], plural(role))) msg.append("\u0002{0}\u0002".format(player))
# FIXME: get rid of hardcoded English
if numrole == 2:
roles_msg.append("The {1} were {0[0]} and {0[1]}.".format(msg, plural(role)))
elif numrole == 1:
roles_msg.append("The {1} was {0[0]}.".format(msg, role))
else:
roles_msg.append("The {2} were {0}, and {1}.".format(", ".join(msg[0:-1]), msg[-1], plural(role)))
message = "" message = ""
count = 0 count = 0
@ -2041,30 +2060,19 @@ def stop_game(var, winner="", abort=False, additional_winners=None, log=True):
channels.Main.send(*roles_msg) channels.Main.send(*roles_msg)
# map player: all roles of that player (for below)
allroles = {player: {role for role, players in rolemap.items() if player in players} for player in mainroles}
# "" indicates everyone died or abnormal game stop # "" indicates everyone died or abnormal game stop
if winner != "" or log: if winner != "" or log:
plrl = {}
pltp = defaultdict(list)
winners = set() winners = set()
player_list = [] player_list = []
if additional_winners is not None: if additional_winners is not None:
winners.update(additional_winners) winners.update(additional_winners)
for role, ppl in var.ORIGINAL_ROLES.items(): for plr, rol in mainroles.items():
if role in var.TEMPLATE_RESTRICTIONS.keys():
for x in ppl:
if x is not None:
pltp[x].append(role)
continue
for x in ppl:
if x is not None:
if x.nick in var.FINAL_ROLES:
plrl[x] = var.FINAL_ROLES[x.nick]
else:
plrl[x] = role
for plr, rol in plrl.items():
orol = rol # original role, since we overwrite rol in case of clone
splr = plr.nick # FIXME: for backwards-compat splr = plr.nick # FIXME: for backwards-compat
pentry = {"nick": None, pentry = {"version": 2,
"nick": None,
"account": None, "account": None,
"ident": None, "ident": None,
"host": None, "host": None,
@ -2081,8 +2089,8 @@ def stop_game(var, winner="", abort=False, additional_winners=None, log=True):
pentry["ident"] = plr.ident pentry["ident"] = plr.ident
pentry["host"] = plr.host pentry["host"] = plr.host
pentry["role"] = rol pentry["mainrole"] = rol
pentry["templates"] = pltp[plr] pentry["allroles"] = allroles[plr]
if splr in var.LOVERS: if splr in var.LOVERS:
pentry["special"].append("lover") pentry["special"].append("lover")
@ -2090,6 +2098,16 @@ def stop_game(var, winner="", abort=False, additional_winners=None, log=True):
iwon = False iwon = False
survived = get_players() survived = get_players()
if not pentry["dced"]: if not pentry["dced"]:
# determine default win status (event can override)
if rol in var.WOLFTEAM_ROLES or (var.DEFAULT_ROLE == "cultist" and role in var.HIDDEN_ROLES):
if winner == "wolves":
won = True
iwon = plr in survived
elif role not in var.TRUE_NEUTRAL_ROLES and winner == "villagers":
won = True
iwon = plr in survived
# true neutral roles are handled via the event below
evt = Event("player_win", {"won": won, "iwon": iwon, "special": pentry["special"]}) evt = Event("player_win", {"won": won, "iwon": iwon, "special": pentry["special"]})
evt.dispatch(var, plr, rol, winner, plr in survived) evt.dispatch(var, plr, rol, winner, plr in survived)
won = evt.data["won"] won = evt.data["won"]
@ -2125,9 +2143,7 @@ def stop_game(var, winner="", abort=False, additional_winners=None, log=True):
# cannot win with dead lover (if splr in survived and lvr is not, that means lvr idled out) # cannot win with dead lover (if splr in survived and lvr is not, that means lvr idled out)
continue continue
lvrrol = "" #somehow lvrrol wasn't set and caused a crash once lvrrol = mainroles[lvuser]
if lvuser in plrl:
lvrrol = plrl[lvuser]
if not winner.startswith("@") and singular(winner) not in var.WIN_STEALER_ROLES: if not winner.startswith("@") and singular(winner) not in var.WIN_STEALER_ROLES:
iwon = True iwon = True
@ -5390,12 +5406,14 @@ def start(cli, nick, chan, forced = False, restart = ""):
return return
for user in var.FORCE_ROLES[role]: for user in var.FORCE_ROLES[role]:
var.MAIN_ROLES[user] = role var.MAIN_ROLES[user] = role
var.ORIGINAL_MAIN_ROLES[user] = role
to_add.add(user) to_add.add(user)
count -= 1 count -= 1
selected = random.sample(vils, count) selected = random.sample(vils, count)
for x in selected: for x in selected:
var.MAIN_ROLES[x] = role var.MAIN_ROLES[x] = role
var.ORIGINAL_MAIN_ROLES[x] = role
vils.remove(x) vils.remove(x)
var.ROLES[role] = UserSet(selected) var.ROLES[role] = UserSet(selected)
var.ROLES[role].update(to_add) var.ROLES[role].update(to_add)
@ -5406,6 +5424,7 @@ def start(cli, nick, chan, forced = False, restart = ""):
var.ROLES[var.DEFAULT_ROLE].update(vils) var.ROLES[var.DEFAULT_ROLE].update(vils)
for x in vils: for x in vils:
var.MAIN_ROLES[x] = var.DEFAULT_ROLE var.MAIN_ROLES[x] = var.DEFAULT_ROLE
var.ORIGINAL_MAIN_ROLES[x] = var.DEFAULT_ROLE
if vils: if vils:
for pr in possible_rolesets: for pr in possible_rolesets:
pr[var.DEFAULT_ROLE] += len(vils) pr[var.DEFAULT_ROLE] += len(vils)