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_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.",
"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_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}.",

View File

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

View File

@ -14,25 +14,35 @@ from src.messages import messages
from src.events import Event
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.
# 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)
# 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"}
if var.AMNESIAC_NIGHTS > 1 and "matchmaker" in roles:
roles.remove("matchmaker")
@event_listener("role_assignment")
def on_role_assignment(evt, var, gamemode, pl):
roles = var.ROLE_GUIDE.keys() - _get_blacklist(var)
for amnesiac in get_all_players(("amnesiac",)):
ROLES[amnesiac] = random.choice(list(roles))
@event_listener("transition_night_begin")
def on_transition_night_begin(evt, var):
global STATS_FLAG
if var.NIGHT_COUNT == var.AMNESIAC_NIGHTS:
amnesiacs = get_all_players(("amnesiac",))
if amnesiacs and not var.HIDDEN_AMNESIAC:
STATS_FLAG = True
for amn in amnesiacs:
role = ROLES[amn]
@ -72,6 +82,7 @@ def on_investigate(evt, var, actor, target):
@event_listener("exchange_roles")
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":
actor_role = ROLES[actor]
if target in ROLES:
@ -94,6 +105,9 @@ def on_exchange_roles(evt, var, actor, target, actor_role, target_role):
@event_listener("revealing_totem")
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":
role = ROLES[votee]
change_role(votee, "amnesiac", role)
@ -106,15 +120,14 @@ def on_revealing_totem(evt, var, votee):
@event_listener("get_reveal_role")
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"
@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":
for player in players:
evt.data["message"].append("\u0002{0}\u0002 (would be {1})".format(player, ROLES[player]))
evt.stop_processing = True
# FIXME: Harcoded English
evt.data["message"].append("would be {0}".format(ROLES[player]))
@event_listener("revealroles_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")
def on_update_stats(evt, var, player, mainrole, revealrole, allroles):
if not var.HIDDEN_AMNESIAC and var.NIGHT_COUNT >= var.AMNESIAC_NIGHTS:
if not var.AMNESIAC_BLACKLIST & {mainrole, revealrole}: # make sure roles aren't blacklisted
evt.data["possible"].add("amnesiac")
if STATS_FLAG and not _get_blacklist(var) & {mainrole, revealrole}:
evt.data["possible"].add("amnesiac")
@event_listener("reset")
def on_reset(evt, var):
global STATS_FLAG
ROLES.clear()
STATS_FLAG = False
# 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.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)
def on_chk_win(evt, var, rolemap, mainroles, lpl, lwolves, lrealwolves):
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["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:

View File

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

View File

@ -73,7 +73,6 @@ var.LAST_GOAT = {}
var.USERS = {}
var.ADMIN_PINGING = False
var.ORIGINAL_ROLES = UserDict() # type: Dict[str, Set[users.User]]
var.DCED_LOSERS = UserSet() # type: Set[users.User]
var.PLAYERS = {}
var.DCED_PLAYERS = {}
@ -84,7 +83,9 @@ var.TIMERS = {}
var.OLD_MODES = defaultdict(set)
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.ORIGINAL_MAIN_ROLES = UserDict() # type: Dict[users.User, str]
var.ALL_PLAYERS = UserList()
var.FORCE_ROLES = DefaultUserDict(UserSet)
@ -340,6 +341,7 @@ def reset():
var.ORIGINAL_ROLES.clear()
var.ROLES["person"] = UserSet()
var.MAIN_ROLES.clear()
var.ORIGINAL_MAIN_ROLES.clear()
var.FORCE_ROLES.clear()
evt = Event("reset", {})
@ -1988,36 +1990,53 @@ def stop_game(var, winner="", abort=False, additional_winners=None, log=True):
roles_msg = []
origroles = {} # user-based list of original roles
with copy.deepcopy(var.ORIGINAL_ROLES) as rolelist:
for role, playerlist 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)
# squirrel away a copy of our original roleset for stats recording, as the following code
# modifies var.ORIGINAL_ROLES and var.ORIGINAL_MAIN_ROLES.
rolecounts = {role: len(players) for role, players in var.ORIGINAL_ROLES.items()}
done = False
for role in role_order():
if len(rolelist[role]) == 0:
continue
evt = Event("get_endgame_message", {"message": [], "done": done})
evt.dispatch(var, role, rolelist[role], origroles)
# save some typing
rolemap = var.ORIGINAL_ROLES
mainroles = var.ORIGINAL_MAIN_ROLES
orig_main = {} # if get_final_role changes mainroles, we want to stash original main role
msg = evt.data["message"]
done = evt.data["done"]
for player, role in mainroles.items():
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:
roles_msg.append("The {1} were {0[0]} and {0[1]}.".format(msg, plural(role)))
elif len(rolelist[role]) == 1:
roles_msg.append("The {1} was {0[0]}.".format(msg, role))
# track if we already printed "was" for a role swap, e.g. The wolves were A (was seer), B (harlot)
# so that we can make the message a bit more concise
roleswap_key = "endgame_roleswap_long"
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:
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 = ""
count = 0
@ -2041,30 +2060,19 @@ def stop_game(var, winner="", abort=False, additional_winners=None, log=True):
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
if winner != "" or log:
plrl = {}
pltp = defaultdict(list)
winners = set()
player_list = []
if additional_winners is not None:
winners.update(additional_winners)
for role, ppl in var.ORIGINAL_ROLES.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
for plr, rol in mainroles.items():
splr = plr.nick # FIXME: for backwards-compat
pentry = {"nick": None,
pentry = {"version": 2,
"nick": None,
"account": None,
"ident": None,
"host": None,
@ -2081,8 +2089,8 @@ def stop_game(var, winner="", abort=False, additional_winners=None, log=True):
pentry["ident"] = plr.ident
pentry["host"] = plr.host
pentry["role"] = rol
pentry["templates"] = pltp[plr]
pentry["mainrole"] = rol
pentry["allroles"] = allroles[plr]
if splr in var.LOVERS:
pentry["special"].append("lover")
@ -2090,6 +2098,16 @@ def stop_game(var, winner="", abort=False, additional_winners=None, log=True):
iwon = False
survived = get_players()
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.dispatch(var, plr, rol, winner, plr in survived)
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)
continue
lvrrol = "" #somehow lvrrol wasn't set and caused a crash once
if lvuser in plrl:
lvrrol = plrl[lvuser]
lvrrol = mainroles[lvuser]
if not winner.startswith("@") and singular(winner) not in var.WIN_STEALER_ROLES:
iwon = True
@ -5390,12 +5406,14 @@ def start(cli, nick, chan, forced = False, restart = ""):
return
for user in var.FORCE_ROLES[role]:
var.MAIN_ROLES[user] = role
var.ORIGINAL_MAIN_ROLES[user] = role
to_add.add(user)
count -= 1
selected = random.sample(vils, count)
for x in selected:
var.MAIN_ROLES[x] = role
var.ORIGINAL_MAIN_ROLES[x] = role
vils.remove(x)
var.ROLES[role] = UserSet(selected)
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)
for x in vils:
var.MAIN_ROLES[x] = var.DEFAULT_ROLE
var.ORIGINAL_MAIN_ROLES[x] = var.DEFAULT_ROLE
if vils:
for pr in possible_rolesets:
pr[var.DEFAULT_ROLE] += len(vils)