Split traitor
As part of this, adjust team and accurate stats to no longer hide any information. For team stats in particular, it was very misleading to list traitor as villager, because it gives the village the impression they have more time than they actually do (i.e. 2 wolfteam 5 vilteam actually means 3 wolfteam 4 vilteam but that is not obvious). As team stats were changed, team reveal was also changed to disregard hidden traitor, so that the two can't be correlated to pick out if traitor died (if traitor was revealed as vilteam but stats decremented wolfteam, that would guarantee that traitor died). Also commit groundwork for the stats rewrite, hidden behind an "experimental" stats type (so it is not on by default). It is still very WIP, many things do not yet work with it.
This commit is contained in:
parent
885889495a
commit
ae5b39c36f
40
src/roles/traitor.py
Normal file
40
src/roles/traitor.py
Normal file
@ -0,0 +1,40 @@
|
||||
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 debuglog, errlog, plog
|
||||
from src.decorators import cmd, event_listener
|
||||
from src.messages import messages
|
||||
from src.events import Event
|
||||
|
||||
@event_listener("get_reveal_role")
|
||||
def on_get_reveal_role(evt, var, nick):
|
||||
# in team reveal, show traitor as wolfteam, otherwise team stats won't sync with how
|
||||
# they're revealed upon death. Team stats should show traitor as wolfteam or else
|
||||
# the stats are wrong in that they'll report one less wolf than actually exists,
|
||||
# which can confuse a lot of people
|
||||
if evt.data["role"] == "traitor" and var.HIDDEN_TRAITOR and var.ROLE_REVEAL != "team":
|
||||
evt.data["role"] = var.DEFAULT_ROLE
|
||||
|
||||
@event_listener("get_final_role")
|
||||
def on_get_final_role(evt, cli, var, nick, role):
|
||||
# if a traitor turns we want to show them as traitor in the end game readout
|
||||
# instead of "wolf (was traitor)"
|
||||
if role == "traitor" and evt.data["role"] == "wolf":
|
||||
evt.data["role"] = "traitor"
|
||||
|
||||
@event_listener("update_stats")
|
||||
def on_update_stats(evt, cli, var, nick, nickrole, nickreveal, nicktpls):
|
||||
if nickrole == var.DEFAULT_ROLE and var.HIDDEN_TRAITOR:
|
||||
evt.data["possible"].add("traitor")
|
||||
# if this is a night death and we know for sure that wolves (and only wolves)
|
||||
# killed, then that kill cannot be traitor as long as they're in wolfchat.
|
||||
# TODO: need to figure out how to actually piece this together, but will
|
||||
# likely require splitting off every other role first.
|
||||
|
||||
# vim: set sw=4 expandtab:
|
@ -359,9 +359,7 @@ def get_roles(*roles):
|
||||
return list(itertools.chain(*all_roles))
|
||||
|
||||
def get_reveal_role(nick):
|
||||
if var.HIDDEN_TRAITOR and get_role(nick) == "traitor":
|
||||
role = var.DEFAULT_ROLE
|
||||
elif var.HIDDEN_AMNESIAC and nick in var.ORIGINAL_ROLES["amnesiac"]:
|
||||
if var.HIDDEN_AMNESIAC and nick in var.ORIGINAL_ROLES["amnesiac"]:
|
||||
role = "amnesiac"
|
||||
elif var.HIDDEN_CLONE and nick in var.ORIGINAL_ROLES["clone"]:
|
||||
role = "clone"
|
||||
|
177
src/wolfgame.py
177
src/wolfgame.py
@ -36,7 +36,7 @@ import threading
|
||||
import time
|
||||
import traceback
|
||||
import urllib.request
|
||||
from collections import defaultdict, deque
|
||||
from collections import defaultdict, deque, Counter
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
@ -302,6 +302,8 @@ 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.ROLE_STATS = frozenset() # type: FrozenSet[FrozenSet[Tuple[str, int]]]
|
||||
var.ROLE_SETS = [] # type: List[Tuple[Counter[str], int]]
|
||||
|
||||
reset_settings()
|
||||
|
||||
@ -1799,12 +1801,50 @@ def stats(cli, nick, chan, rest):
|
||||
else:
|
||||
message.append("\u0002{0}-{1}\u0002 {2}".format(count[0], count[1], plural(role)))
|
||||
|
||||
# Experimental replacement for default, not yet ready for prime-time
|
||||
# Uses events in order to enable roles to modify logic
|
||||
# The events are fired off as part of transition_day and del_player, and are not calculated here
|
||||
elif var.STATS_TYPE == "experimental":
|
||||
# Collapse var.ROLE_STATS into a Dict[str, Tuple[int, int]]
|
||||
role_stats = {}
|
||||
for stat_set in var.ROLE_STATS:
|
||||
for r, a in stat_set:
|
||||
if r not in role_stats:
|
||||
role_stats[r] = (a, a)
|
||||
else:
|
||||
mn, mx = role_stats[r]
|
||||
role_stats[r] = (min(mn, a), max(mx, a))
|
||||
start_roles = set()
|
||||
for r, v in var.ORIGINAL_ROLES.items():
|
||||
if r in var.TEMPLATE_RESTRICTIONS or len(v) == 0:
|
||||
continue
|
||||
start_roles.add(r)
|
||||
for r in start_roles:
|
||||
if r not in role_stats:
|
||||
role_stats[r] = (0, 0)
|
||||
order = [r for r in role_order() if r in role_stats]
|
||||
if var.DEFAULT_ROLE in order:
|
||||
order.remove(var.DEFAULT_ROLE)
|
||||
order.append(var.DEFAULT_ROLE)
|
||||
first = role_stats[order[0]]
|
||||
if first[0] == first[1] == 1:
|
||||
vb = "is"
|
||||
else:
|
||||
vb = "are"
|
||||
|
||||
# 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.
|
||||
for role in order:
|
||||
count = role_stats[role]
|
||||
if count[0] == count[1]:
|
||||
if count[0] != 1:
|
||||
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", 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], plural(role)))
|
||||
|
||||
# Show everything as-is, with no hidden information
|
||||
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]]
|
||||
@ -1816,42 +1856,11 @@ def stats(cli, nick, chan, rest):
|
||||
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:
|
||||
@ -1869,6 +1878,7 @@ def stats(cli, nick, chan, rest):
|
||||
# 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"
|
||||
# as accurate, this contains no hidden information
|
||||
elif var.STATS_TYPE == "team":
|
||||
wolfteam = 0
|
||||
villagers = 0
|
||||
@ -1877,28 +1887,13 @@ def stats(cli, nick, chan, rest):
|
||||
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)
|
||||
if role in var.WOLFTEAM_ROLES:
|
||||
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"))
|
||||
@ -2192,6 +2187,8 @@ def show_votes(cli, nick, chan, rest):
|
||||
|
||||
reply(cli, nick, chan, the_message)
|
||||
|
||||
# TODO: need to generalize this logic (as well as the logic in chk_win_conditions)
|
||||
# once refactored, it should be split off into individual role files
|
||||
def chk_traitor(cli):
|
||||
realwolves = var.WOLF_ROLES - {"wolf cub"}
|
||||
if len(list_players(realwolves)) > 0:
|
||||
@ -2250,6 +2247,7 @@ def stop_game(cli, winner="", abort=False, additional_winners=None, log=True):
|
||||
|
||||
origroles = {} #nick based list of original roles
|
||||
rolelist = copy.deepcopy(var.ORIGINAL_ROLES)
|
||||
event = Event("get_final_role", {"role": None})
|
||||
for role, playerlist in var.ORIGINAL_ROLES.items():
|
||||
if role in var.TEMPLATE_RESTRICTIONS.keys():
|
||||
continue
|
||||
@ -2257,11 +2255,13 @@ def stop_game(cli, winner="", abort=False, additional_winners=None, log=True):
|
||||
player = p #with (dced) still in
|
||||
if p.startswith("(dced)"):
|
||||
p = p[6:]
|
||||
# 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")):
|
||||
event.data["role"] = var.FINAL_ROLES.get(p, role)
|
||||
event.dispatch(cli, var, p, role)
|
||||
# TODO: make cub use the event instead of hardcoding it here
|
||||
if role != event.data["role"] and (event.data["role"] != "wolf" or role != "wolf cub"):
|
||||
origroles[p] = role
|
||||
rolelist[role].remove(player)
|
||||
rolelist[var.FINAL_ROLES[p]].add(p)
|
||||
rolelist[event.data["role"]].add(p)
|
||||
prev = False
|
||||
for role in role_order():
|
||||
if len(rolelist[role]) == 0:
|
||||
@ -2704,6 +2704,7 @@ def del_player(cli, nick, forced_death=False, devoice=True, end_game=True, death
|
||||
pl.remove(dead)
|
||||
if nick != None and (nick == original or nick in pl):
|
||||
nickrole = get_role(nick)
|
||||
nickreveal = get_reveal_role(nick)
|
||||
nicktpls = get_templates(nick)
|
||||
var.ROLES[nickrole].remove(nick)
|
||||
for t in nicktpls:
|
||||
@ -2974,6 +2975,29 @@ def del_player(cli, nick, forced_death=False, devoice=True, end_game=True, death
|
||||
ismain=ismain, refresh_pl=refresh_pl, del_player=del_player)
|
||||
event.dispatch(cli, var, nick, nickrole, nicktpls, evt_death_triggers)
|
||||
|
||||
# update var.ROLE_STATS
|
||||
event = Event("update_stats", {"possible": {nickrole, nickreveal}, "known_role": False})
|
||||
event.dispatch(cli, var, nick, nickrole, nickreveal, nicktpls)
|
||||
# Given the set of possible roles this nick could be (or its actual role if known_role is True),
|
||||
# figure out the set of roles that need deducting from their counts in var.ROLE_STATS
|
||||
if event.data["known_role"]:
|
||||
# we somehow know the exact role that died (for example, we know traitor died even though they revealed as villager)
|
||||
# as a result, deduct only them
|
||||
possible = {nickrole}
|
||||
else:
|
||||
possible = set(event.data["possible"])
|
||||
newstats = set()
|
||||
# For every possible role this person is, try to deduct 1 from that role's count in our stat sets
|
||||
# if a stat set doesn't contain the role, then that would lead to an impossible condition and therefore
|
||||
# that set is not added to newstats to indicate that set is no longer possible
|
||||
for p in possible:
|
||||
for rs in var.ROLE_STATS:
|
||||
d = dict(rs)
|
||||
if p in d and d[p] >= 1:
|
||||
d[p] -= 1
|
||||
newstats.add(frozenset(d.items()))
|
||||
var.ROLE_STATS = frozenset(newstats)
|
||||
|
||||
if devoice and (var.PHASE != "night" or not var.DEVOICE_DURING_NIGHT):
|
||||
cmode.append(("-v", nick))
|
||||
if users.exists(nick):
|
||||
@ -6062,6 +6086,28 @@ def start(cli, nick, chan, forced = False, restart = ""):
|
||||
cli.msg(chan, messages["no_settings_defined"].format(nick, len(villagers)))
|
||||
return
|
||||
|
||||
possible_rolesets = []
|
||||
roleset_roles = defaultdict(int)
|
||||
for rs, amt in var.ROLE_SETS:
|
||||
toadd = random.sample(list(rs.elements()), amt)
|
||||
for r in toadd:
|
||||
addroles[r] += 1
|
||||
roleset_roles[r] += 1
|
||||
add_rolesets = []
|
||||
temp_rolesets = []
|
||||
for c in itertools.combinations(rs.elements(), amt):
|
||||
add_rolesets.append(Counter(c))
|
||||
for pr in possible_rolesets:
|
||||
for ar in add_rolesets:
|
||||
temp = Counter(pr)
|
||||
temp.update(ar)
|
||||
temp_rolesets.append(temp)
|
||||
possible_rolesets = temp_rolesets
|
||||
if not possible_rolesets:
|
||||
# if there are no randomized roles, ensure that we have 1 element
|
||||
# to account for the only possibility (all role counts known)
|
||||
possible_rolesets.append(Counter())
|
||||
|
||||
if var.ORIGINAL_SETTINGS and not restart: # Custom settings
|
||||
need_reset = True
|
||||
wvs = sum(addroles[r] for r in var.WOLFCHAT_ROLES)
|
||||
@ -6144,12 +6190,25 @@ def start(cli, nick, chan, forced = False, restart = ""):
|
||||
var.ROLES[role] = [None] * count
|
||||
continue # We deal with those later, see below
|
||||
selected = random.sample(villagers, count)
|
||||
var.ROLES[role] = set(selected)
|
||||
for x in selected:
|
||||
villagers.remove(x)
|
||||
|
||||
var.ROLES[role] = set(selected)
|
||||
fixed_count = count - roleset_roles[role]
|
||||
if fixed_count > 0:
|
||||
for pr in possible_rolesets:
|
||||
pr[role] += fixed_count
|
||||
for v in villagers:
|
||||
var.ROLES[var.DEFAULT_ROLE].add(v)
|
||||
if villagers:
|
||||
for pr in possible_rolesets:
|
||||
pr[var.DEFAULT_ROLE] += len(villagers)
|
||||
|
||||
# Collapse possible_rolesets into var.ROLE_STATS
|
||||
# which is a FrozenSet[FrozenSet[Tuple[str, int]]]
|
||||
possible_rolesets_set = set()
|
||||
for pr in possible_rolesets:
|
||||
possible_rolesets_set.add(frozenset(pr.items()))
|
||||
var.ROLE_STATS = frozenset(possible_rolesets_set)
|
||||
|
||||
# Now for the templates
|
||||
for template, restrictions in var.TEMPLATE_RESTRICTIONS.items():
|
||||
|
Loading…
x
Reference in New Issue
Block a user