Skip to content

Commit

Permalink
Overhaul role/mode/totem localization
Browse files Browse the repository at this point in the history
- New completion commands for each in src.functions
- Removed old completion commands from src.utilities
- New specs that sort localized values in messages
- Use localized modes instead of internal names everywhere (I think)

Fixes #460
  • Loading branch information
skizzerz committed Dec 15, 2020
1 parent ae6ce17 commit b5f8515
Show file tree
Hide file tree
Showing 19 changed files with 452 additions and 231 deletions.
15 changes: 8 additions & 7 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
"lycan": "lycan",
"mad": "mad",
"maelstrom": "maelstrom",
"masquerade": "masquerade",
"mudkip": "mudkip",
"noreveal": "noreveal",
"random": "random",
Expand Down Expand Up @@ -565,7 +566,7 @@
"you_not_in_stasis": "You are not currently in stasis.",
"not_playing": "{0:bold} is not currently playing.",
"not_playing_suggestions": "{0:bold} is not currently playing. Perhaps you meant: {1:join_simple(bold)}",
"ambiguous_mode": "{0:bold} is not a valid game mode. Valid modes are: {1:join}",
"ambiguous_mode": "Ambiguous game mode. Possible matches are: {0:join}.",
"not_logged_in": "You are not logged in to NickServ.",
"notice_off": "Gameplay interactions will now use PRIVMSG for you.",
"notice_on": "The bot will now always NOTICE you.",
Expand All @@ -585,8 +586,8 @@
"set_pingif": "Your ping preferences have been set to {0}.",
"pingif_invalid": "Invalid parameter. Please enter a non-negative integer or a valid preference.",
"ping_player": "PING! {0} {=player,players:plural({0})}! ",
"already_voted_game": "You have already voted for the {0} game mode.",
"vote_game_mode": "{0:@} votes for the {1:bold} game mode.",
"already_voted_game": "You have already voted for the {0!mode} game mode.",
"vote_game_mode": "{0:@} votes for the {1!mode:bold} game mode.",
"you_already_playing": "You're already playing!",
"other_already_playing": "They're already playing!",
"too_many_players": "Too many players! Try again next time.",
Expand Down Expand Up @@ -634,7 +635,7 @@
"wolves_list": "Wolves: {0}",
"players_list": "Players: {0:join}",
"players_list_count": "{0:bold} {=player,players:plural({0})}: {1:join}",
"players_list_entry": "{0:{1}}[if={2}] ({2:join_simple})[/if]",
"players_list_entry": "{0:{1}}[if={2}] ({2:join_simple(!role)})[/if]",
"stats_reply": "It is currently {0!phase}. There {=is,are:plural({1})} {2:join}.",
"stats_reply_entry_none": "no {0!role:plural(0)}",
"stats_reply_entry_single": "{1:bold} {0!role:plural({1})}",
Expand Down Expand Up @@ -1101,7 +1102,7 @@
"invalid_fsend_permissions": "You do not have permission to message this user or channel.",
"temp_invalid_perms": "You are not allowed to use that command right now.",
"fgame_success": "{0:@} has changed the game settings successfully.",
"available_mode_setters": "Available game mode setters: {0:join}",
"available_mode_setters": "Available game mode setters: {0:sort(!mode)}",
"setter_not_found": "Game mode setter {0:bold} not found.",
"setter_no_doc": "Game mode {0} has no doc string.",
"invalid_target": "This can only be done on players in the channel or fake nicks.",
Expand Down Expand Up @@ -1160,8 +1161,8 @@
"cult_leader_notify": "You are {=cult leader!role:article} {=cult leader!role:bold}. It is your job to help the wolves kill all of the villagers.",
"blessed_notify": "You are [b]blessed[/b] by a benevolent power. The first attempt to kill you each night will fail.",
"blessed_myrole": "You are {=blessed villager!role:article} {=blessed villager!role:bold}.",
"welcome_simple": "{0:join}: Welcome to Werewolf, the popular detective/social party game (a theme of Mafia). Using the {1:bold} game mode.",
"welcome_options": "{0:join}: Welcome to Werewolf, the popular detective/social party game (a theme of Mafia). Using the {1:bold} game mode with {2:join}.",
"welcome_simple": "{0:join}: Welcome to Werewolf, the popular detective/social party game (a theme of Mafia). Using the {1!mode:bold} game mode.",
"welcome_options": "{0:join}: Welcome to Werewolf, the popular detective/social party game (a theme of Mafia). Using the {1!mode:bold} game mode with {2:join}.",
"gso_rr_on": "role reveal",
"gso_rr_team": "team reveal",
"gso_rr_off": "no role reveal",
Expand Down
2 changes: 1 addition & 1 deletion src/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
# The top line must only depend on things imported above in our "no dependencies" block
# All botconfig and settings are fully established at this point and are safe to use

from src import cats, messages
from src import messages, cats
from src import context, functions, utilities
from src import db
from src import users
Expand Down
6 changes: 3 additions & 3 deletions src/cats.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from typing import Dict
import itertools

from src import events
from src.events import Event, EventListener

__all__ = [
"get", "role_order",
Expand Down Expand Up @@ -73,7 +73,7 @@ def role_order():

def _register_roles(evt):
global FROZEN
mevt = events.Event("get_role_metadata", {})
mevt = Event("get_role_metadata", {})
mevt.dispatch(None, "role_categories")
for role, cats in mevt.data.items():
if len(cats & {"Wolfteam", "Village", "Neutral", "Hidden"}) != 1:
Expand All @@ -89,7 +89,7 @@ def _register_roles(evt):
cat.freeze()
FROZEN = True

events.EventListener(_register_roles, priority=1).install("init")
EventListener(_register_roles, priority=1).install("init")

class Category:
"""Base class for role categories."""
Expand Down
4 changes: 2 additions & 2 deletions src/db/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import botconfig
import src.settings as var
from src.utilities import singular
from src.messages import messages, get_role_name
from src.messages import messages, LocalRole
from src.cats import role_order

# increment this whenever making a schema change so that the schema upgrade functions run on start
Expand Down Expand Up @@ -377,7 +377,7 @@ def get_game_stats(mode, size):
bits = []
for row in c:
winner = singular(row[0])
winner = get_role_name(winner, number=None).title()
winner = LocalRole(winner).singular.title()
if not winner:
winner = botconfig.NICK.title()
bits.append(messages["db_gstats_win"].format(winner, row[1], row[1]/total_games))
Expand Down
2 changes: 1 addition & 1 deletion src/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ def __exit__(self, exc_type, exc_value, tb):
message = [str(messages["error_log"])]

link = _tracebacks.get("\n".join(variables))
if link is None:
if link is None and not botconfig.DEBUG_MODE:
api_url = "https://ww.chat/submit"
data = None
with _local.handler:
Expand Down
106 changes: 102 additions & 4 deletions src/functions.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
from typing import Optional
from typing import Optional, Iterable
from collections import Counter
from src.messages import messages
import functools

from src.messages import messages, LocalRole, LocalMode, LocalTotem
from src.events import Event
from src.cats import Wolfteam, Neutral, Hidden
from src.cats import Wolfteam, Neutral, Hidden, All
from src.match import Match, match_all
from src import settings as var

__all__ = [
"get_players", "get_all_players", "get_participants",
"get_target", "change_role",
"get_main_role", "get_all_roles", "get_reveal_role",
"match_role", "match_mode", "match_totem"
]

def get_players(roles=None, *, mainroles=None):
Expand Down Expand Up @@ -159,4 +163,98 @@ def get_reveal_role(user):
else:
return "village member"

# vim: set sw=4 expandtab:
def match_role(var, role: str, remove_spaces: bool = False, allow_special: bool = True, scope: Optional[Iterable[str]] = None) -> Match[LocalRole]:
""" Match a partial role or alias name into the internal role key.
:param var: Game state
:param role: Partial role to match on
:param remove_spaces: Whether or not to remove all spaces before matching.
This is meant for contexts where we truly cannot allow spaces somewhere; otherwise we should
prefer that the user matches including spaces where possible for friendlier-looking commands.
:param allow_special: Whether to allow special keys (lover, vg activated, etc.).
If scope is set, this parameter is ignored.
:param scope: Limit matched roles to these explicitly passed-in roles (iterable of internal role names).
:return: Match object with all matches (see src.match.match_all)
"""
role = role.lower()
if remove_spaces:
role = role.replace(" ", "")

role_map = messages.get_role_mapping(reverse=True, remove_spaces=remove_spaces)

special_keys = set()
if scope is None and allow_special:
evt = Event("get_role_metadata", {})
evt.dispatch(var, "special_keys")
special_keys = functools.reduce(lambda x, y: x | y, evt.data.values(), special_keys)

matches = match_all(role, role_map.keys())

# strip matches that don't refer to actual roles or special keys (i.e. refer to team names)
filtered_matches = set()
if scope is not None:
allowed = set(scope)
else:
allowed = All.roles | special_keys

for match in matches:
if role_map[match] in allowed:
filtered_matches.add(LocalRole(role_map[match], match))

return Match(filtered_matches)

def match_mode(var, mode: str, remove_spaces: bool = False, scope: Optional[Iterable[str]] = None) -> Match[LocalMode]:
""" Match a partial game mode into the internal game mode key.
:param var: Game state
:param mode: Partial game mode to match on
:param remove_spaces: Whether or not to remove all spaces before matching.
This is meant for contexts where we truly cannot allow spaces somewhere; otherwise we should
prefer that the user matches including spaces where possible for friendlier-looking commands.
:param scope: Limit matched modes to these explicitly passed-in modes (iterable of internal mode names).
:return: Match object with all matches (see src.match.match_all)
"""
mode = mode.lower()
if remove_spaces:
mode = mode.replace(" ", "")

mode_map = messages.get_mode_mapping(reverse=True, remove_spaces=remove_spaces)
matches = match_all(mode, mode_map.keys())

# strip matches that aren't in scope, and convert to LocalMode objects
filtered_matches = set()
if scope is not None:
allowed = set(scope)
else:
allowed = set(var.GAME_MODES)

for match in matches:
if mode_map[match] in allowed:
filtered_matches.add(LocalMode(mode_map[match], match))

return Match(filtered_matches)

def match_totem(var, totem: str, scope: Optional[Iterable[str]] = None) -> Match[LocalTotem]:
""" Match a partial totem into the internal totem key.
:param var: Game state
:param totem: Partial totem to match on
:param scope: Limit matched modes to these explicitly passed-in totems (iterable of internal totem names).
:return: Match object with all matches (see src.match.match_all)
"""
mode = totem.lower()
totem_map = messages.get_totem_mapping(reverse=True)
matches = match_all(totem, totem_map.keys())

# strip matches that aren't in scope, and convert to LocalMode objects
filtered_matches = set()
if scope is not None:
allowed = set(scope)
else:
allowed = set(totem_map.keys())

for match in matches:
if totem_map[match] in allowed:
filtered_matches.add(LocalTotem(totem_map[match], match))

return Match(filtered_matches)
11 changes: 5 additions & 6 deletions src/gamemodes/boreal.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
from src.gamemodes import game_mode, GameMode, InvalidModeException
from src.roles.helper.wolves import send_wolfchat_message
from src.messages import messages
from src.functions import get_players, get_all_players, get_main_role, change_role
from src.utilities import complete_one_match
from src.functions import get_players, get_all_players, get_main_role, change_role, match_totem
from src.events import EventListener, find_listener
from src.containers import DefaultUserDict
from src.status import add_dying
Expand Down Expand Up @@ -254,20 +253,20 @@ def feed(self, var, wrapper, message):
from src.roles.wolfshaman import TOTEMS as ws_totems, SHAMANS as ws_shamans

pieces = re.split(" +", message)
valid = ("sustenance", "hunger")
valid = {"sustenance", "hunger"}
state_vars = ((s_totems, s_shamans), (ws_totems, ws_shamans))
for TOTEMS, SHAMANS in state_vars:
if wrapper.source not in TOTEMS:
continue

totem_types = list(TOTEMS[wrapper.source].keys())
given = complete_one_match(pieces[0], totem_types)
totem_types = set(TOTEMS[wrapper.source].keys()) & valid
given = match_totem(var, pieces[0], scope=totem_types)
if not given and TOTEMS[wrapper.source].get("sustenance", 0) + TOTEMS[wrapper.source].get("hunger", 0) > 1:
wrapper.send(messages["boreal_ambiguous_feed"])
return

for totem in valid:
if (given and totem != given) or TOTEMS[wrapper.source].get(totem, 0) == 0:
if (given and totem != given.get().key) or TOTEMS[wrapper.source].get(totem, 0) == 0:
continue # doesn't have a totem that can be used to feed tribe

SHAMANS[wrapper.source][totem].append(users.Bot)
Expand Down
9 changes: 4 additions & 5 deletions src/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
import src.settings as var
from src import decorators, wolfgame, channels, users, errlog as log, stream_handler as alog
from src.messages import messages
from src.functions import get_participants, get_all_roles
from src.utilities import complete_role
from src.functions import get_participants, get_all_roles, match_role
from src.dispatcher import MessageDispatcher
from src.decorators import handle_error, command, hook
from src.context import Features
Expand Down Expand Up @@ -113,11 +112,11 @@ def parse_and_dispatch(var,

if role_prefix is not None:
# match a role prefix to a role. Multi-word roles are supported by stripping the spaces
matches = complete_role(var, role_prefix, remove_spaces=True)
matches = match_role(var, role_prefix, remove_spaces=True)
if len(matches) == 1:
role_prefix = matches[0]
role_prefix = matches.get().key
elif len(matches) > 1:
wrapper.pm(messages["ambiguous_role"].format(matches))
wrapper.pm(messages["ambiguous_role"].format([m.singular for m in matches]))
return
else:
wrapper.pm(messages["no_such_role"].format(role_prefix))
Expand Down
33 changes: 31 additions & 2 deletions src/match.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Generic, Iterable, TypeVar
from typing import Generic, Iterable, TypeVar, Optional

__all__ = ["Match"]
__all__ = ["Match", "match_all", "match_one"]

T = TypeVar("T")

Expand All @@ -21,3 +21,32 @@ def get(self) -> T:
if len(self._matches) != 1:
raise ValueError("Can only call get on a match with a single result")
return self._matches[0]

def match_all(search: str, scope: Iterable[str]) -> Match[str]:
""" Retrieve all items that begin with a search term.
:param search: Term to search for (prefix)
:param scope: Items to search for matches
:return: Match object constructed as follows:
If search exactly equals an item in scope, it will be the only returned value.
Otherwise, all items that begin with search will be returned.
"""
found = set()
for item in scope:
if search == item:
found = {item}
break
if item.startswith(search):
found.add(item)
return Match(found)

def match_one(search: str, scope: Iterable[str]) -> Optional[str]:
""" Retrieve a single item that begins with the search term.
:param search: Term to search for (prefix)
:param scope: Items to search for matches
:return: If search matches exactly one item (as per the return value of match_all),
returns that item. Otherwise, returns None.
"""
m = match_all(search, scope)
return m.get() if m else None
Loading

0 comments on commit b5f8515

Please sign in to comment.