Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Relax game name matching when creating a table #41

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ kill:
@kill `cat pid` 2>/dev/null || true
run: kill
@python3 -u src/main.py 2>&1 >> errs & echo $$! > pid

test: export PYTHONPATH=src
test:
@python3 -m unittest discover -s tests
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
aiohttp
cryptography
discord.py==1.5.0
num2words
unidecode
48 changes: 20 additions & 28 deletions src/bga_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
import urllib.parse

import aiohttp
from bga_game_list import get_game_list
from bga_game_list import get_games_by_name_part, get_id_by_game, get_title_by_game
from utils import simplify_name


logging.getLogger("aiohttp").setLevel(logging.WARN)

Expand Down Expand Up @@ -128,37 +130,27 @@ async def create_table(self, game_name_part):
"""Create a table and return its url. 201,0 is to set to normal mode.
Partial game names are ok, like race for raceforthegalaxy.
Returns (table id (int), error string (str))"""

# Try to close any logged-in session gracefully
lower_game_name = re.sub(r"[^a-z0-9]", "", game_name_part.lower())
await self.quit_table()
await self.quit_playing_with_friends()
games, err_msg = await get_game_list()
if len(err_msg) > 0:
return -1, err_msg
lower_games = {}
for game in games:
lower_name = re.sub(r"[^a-z0-9]", "", game.lower())
lower_games[lower_name] = games[game]

# If name is unique like "race" for "raceforthegalaxy", use that
games_found = []
game_name = ""
for game_i in list(lower_games.keys()):
if game_i == lower_game_name: # if there's an exact match, take it!
game_name = lower_game_name
elif game_i.startswith(lower_game_name):
games_found.append(game_i)
if len(game_name) == 0:
if len(games_found) == 0:
err = (
f"`{lower_game_name}` is not available on BGA. Check your spelling "
f"(capitalization and special characters do not matter)."
)
return -1, err
elif len(games_found) > 1:
err = f"`{lower_game_name}` matches [{','.join(games_found)}]. Use more letters to match."
return -1, err
game_name = games_found[0]
game_id = lower_games[game_name]
games_found = await get_games_by_name_part(game_name_part)
if len(games_found) == 0:
err = (
f"`{simplify_name(game_name_part)}` is not available on BGA. Check your spelling "
f"(capitalization and special characters do not matter)."
)
return -1, err
elif len(games_found) > 1:
games_found_md = "\n"
for game in games_found:
games_found_md += f"• `{simplify_name(await get_title_by_game(game))}`" + "\n"
err = f"`{simplify_name(game_name_part)}` matches:{games_found_md}Use more letters to match."
return -1, err
game_id = await get_id_by_game(games_found[0])

url = self.base_url + "/table/table/createnew.html"
params = {
"game": game_id,
Expand Down
5 changes: 1 addition & 4 deletions src/bga_game_list.json
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,6 @@
"Yin Yang": 1226,
"Yokai": 1181,
"Yokohama": 1161,
"welcometo": "1300",
"Tichu": 1237,
"sevenwondersduelagora": "1279",
"carcassonnehuntersandgatherers": "1276",
"lettertycoon": "1273"
"sevenwondersduelagora": "1279"
}
50 changes: 44 additions & 6 deletions src/bga_game_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import aiohttp

from utils import normalize_name
from utils import normalize_name, simplify_name

logging.getLogger("aiohttp").setLevel(logging.WARN)

Expand Down Expand Up @@ -88,11 +88,49 @@ def update_games_cache(games):
f.write(json.dumps(games, indent=2) + "\n")


async def is_game_valid(game):
# Check if any words are games
async def is_game_valid(name):
return normalize_name(name) in await get_simplified_game_list()


async def get_simplified_game_list():
games, errs = await get_game_list()
if errs:
games, errs = get_game_list_from_cache()

simplified_games = {}
for full_name in games:
simplified_games[normalize_name(full_name)] = simplify_name(full_name)
return simplified_games


async def get_id_by_game(normalized_name):
games, errs = await get_game_list()
if errs:
games, errs = get_game_list_from_cache()

for title, id in games.items():
if normalize_name(title) == normalized_name:
return id


async def get_title_by_game(normalized_name):
games, errs = await get_game_list()
if errs:
games, errs = get_game_list_from_cache()
normalized_games = [normalize_name(g) for g in games]
normalized_game = normalize_name(game)
return normalized_game in normalized_games

for title in games:
if normalize_name(title) == normalized_name:
return title


async def get_games_by_name_part(name_part):
simplified_name_part = simplify_name(name_part)
simplified_games = await get_simplified_game_list()
games = []

for normalized_name, simplified_name in simplified_games.items():
if simplified_name == simplified_name_part: # if there's an exact match, take it!
return [normalized_name]
elif simplified_name.startswith(simplified_name_part):
games.append(normalized_name)
return games
25 changes: 25 additions & 0 deletions src/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""Utils for various parts of this program"""
from num2words import num2words
from unidecode import unidecode
from urllib.parse import urlparse
import re

Expand Down Expand Up @@ -49,9 +51,32 @@ async def send_message_partials(destination, remainder):


def normalize_name(game_name):
"""Generate a string that can be used to uniquely identify a game."""
return re.sub("[^a-z0-7]+", "", game_name.lower())


def simplify_name(game_name):
"""Generate a string that can be used for comparing/matching the name of a game in a more reliable way than using user input directly."""
game_name = game_name.lower()
game_name = unidecode(game_name)
game_name = re.sub(r"\s+", " ", game_name)
game_name = re.sub(r"^the ", "", game_name)
game_name = re.sub(r"[!(].*", "", game_name)
if not re.search(
r"\b(?:builders|carcassonne|through the ages)\b",
game_name
):
game_name = re.sub(r":.*", "", game_name)
game_name = re.sub(r"^voyages of ", "", game_name)
game_name = re.sub(r"of miller.?s +hollow$", "", game_name)
game_name = re.sub(r" & " , " and ", game_name)
game_name = re.sub(r"\bii\b" , "two", game_name)
game_name = re.sub(r"\d+", lambda m: num2words(m.group()), game_name)
game_name = re.sub(r"[^a-z]+", "", game_name)

return game_name


def force_double_quotes(string):
# People from other countries keep on using strange quotes because of their phone's keyboard
# Force double quotes so shlex parses correctly
Expand Down
58 changes: 58 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from bga_game_list import get_game_list_from_cache
from utils import simplify_name
import unittest


class TestUtils(unittest.IsolatedAsyncioTestCase):
async def asyncSetUp(self):
self.games, errs = await get_game_list_from_cache()

def test_simplify_name(self):
# Exercise all of the existing simplifications rules:
self.assertEqual(simplify_name("Gaïa"), "gaia")
self.assertEqual(simplify_name("6 Nimmt!"), "sixnimmt")
self.assertEqual(
simplify_name("The Jelly Monster Lab"),
"jellymonsterlab"
)
self.assertEqual(
simplify_name("99 (trick-taking card game)"),
"ninetynine"
)
self.assertEqual(
simplify_name("Unconditional Surrender! World War 2 in Europe "),
"unconditionalsurrender"
)
self.assertEqual(
simplify_name("The Builders: Middle Ages"),
"buildersmiddleages"
)
self.assertEqual(simplify_name("Through the Ages"), "throughtheages")
self.assertEqual(
simplify_name("Through the Ages: A new Story of Civilization"),
"throughtheagesanewstoryofcivilization"
)
self.assertEqual(
simplify_name("Marco Polo II: In the Service of the Khan"),
"marcopolotwo"
)
self.assertEqual(
simplify_name("The Voyages of Marco Polo"),
"marcopolo"
)
self.assertEqual(
simplify_name("The Werewolves of Miller's Hollow"),
"werewolves"
)
self.assertEqual(simplify_name("Gear & Piston"), "gearandpiston")

# Check for any collisions between simplified names:
simplified_names = set()
for full_name in self.games:
simplified_name = simplify_name(full_name)
self.assertNotIn(simplified_name, simplified_names)
simplified_names.add(simplified_name)


if __name__ == '__main__':
unittest.main()