Move shiritori into a class as it was getting complicated

This commit is contained in:
Dan Hess 2021-03-26 12:33:23 -08:00
parent f048e532e0
commit e050b3ba77
2 changed files with 179 additions and 88 deletions

View File

@ -1,9 +1,178 @@
from dataclasses import dataclass
from discord.ext import commands
import collections
import utils
@dataclass
class JPWord:
kanji: str
reading: str
def __eq__(self, other):
return (
isinstance(other, JPWord)
and self.kanji == other.kanji
and self.reading == other.reading
)
def __hash__(self):
return hash(self.kanji + self.reading)
class Shiritori:
def __init__(self):
self.losers = []
self.players = []
self.last_kana = None
self.last_player = None
self.used_words = []
# This won't include small tsu because for this use case
small_kanas = ["", "", "", "", "", "", "", ""]
def get_first_kana(self, word: JPWord):
i = iter(word.reading)
for char in i:
# I don't know why someone would do it, but something like 「りんご」
# I'd want to ignore the []
if char.isalpha():
# If there is a small kana, like りゅ then we want both
try:
n = next(i)
except StopIteration:
return char
else:
if n in self.small_kanas:
return f"{char}{n}"
else:
return char
def get_last_kana(self, word: JPWord):
i = reversed(word.reading)
for char in i:
# Ignore anything in case they end with something like or
if char.isalpha():
# If the last one is a small kana (other than つ)
if char in self.small_kanas:
return f"{next(i)}{char}"
else:
return char
def cull_words(self, words: list):
"""Removes readings that will not fit based on last used kana"""
c = words.copy()
for word in words:
first = self.get_first_kana(word)
if self.last_kana is not None and first != self.last_kana:
c.remove(word)
return c
async def player_lost(self, ctx: commands.Context, msg: str = None):
"""Sends a player lost message, tracks players losing, and returns
based on whether or not the game is now over"""
self.losers.append(ctx.author)
self.players.remove(ctx.author)
if msg:
await ctx.send(msg)
return len(self.players) == 1
async def readings_for_word(self, word: str):
"""This goes through the list that jisho API gives us and returns the right reading and word
This does no validation of kana, this is only for the proper kanji/reading pairs that the player
was possibly meaning to use"""
data = await utils.request(
"https://jisho.org/api/v1/search/words", payload={"keyword": word}
)
possibilities = set()
for slug in data["data"]:
is_noun = False
# Determinte if this slug is a noun
for sense in slug["senses"]:
if "Noun" in sense["parts_of_speech"]:
is_noun = True
break
# If this isn't a noun we don't even care
if not is_noun:
continue
# Now go through definitions to get readings/kanji
for japanese in slug["japanese"]:
# They can enter either the kanji or the reading they want, compare both
_word = japanese.get("word")
reading = japanese.get("reading")
# If there are readings of this word that have a paired kanji, prefer that
if _word is None and possibilities:
continue
elif _word is None:
possibilities.add(JPWord("", reading))
# If the provided kanji/reading is right, then add it as a possibility
if _word == word or reading == word:
possibilities.add(JPWord(_word, reading))
return list(possibilities)
async def validate_word(self, ctx: commands.Context, word: str):
# First make sure the we ensure the player is tracked
if ctx.author in self.losers:
await ctx.message.add_reaction("")
return False
if ctx.author not in self.players:
self.players.append(ctx.author)
# Get the words that could have been meant
words = await self.readings_for_word(word)
# Validate that any words were returned
if not words:
msg = f"{ctx.author} loses, only Japanese nouns can be used"
# msg += f"\n{ctx.author}の勝利。日本語の名詞のみ使用できる"
return await self.player_lost(ctx, msg)
# Validate there are still words, if not then the last kana wasn't right
words = self.cull_words(words)
if self.last_kana is not None and not words:
msg = f'{ctx.author} loses, you must enter a word that begins with "{self.last_kana}"'
# msg += f"\n{ctx.author}の勝利。「{self.last_kana}」から始まる言葉を入力しなくてはなりません"
return await self.player_lost(ctx, msg)
# Validate it's more than one syllable
words = [word for word in words if len(word.reading) > 1]
if not words:
msg = f"{ctx.author} please only use words with more than one syllable"
# msg += f"\n{ctx.author}、複数の音節を持つ単語のみを使用してください"
await ctx.send(msg)
return False
# If we're here, we have one (or more, but we care about the first) matching word
# just base on that from now on
word = words[0]
# Validate against words used
if word in self.used_words:
msg = f"{ctx.author} loses, {word} has already been used"
# msg += f"\n{ctx.author}の勝利。「{word}」は既に使用されている"
return await self.player_lost(ctx, msg)
# Validate against ん and ン
last_kana = self.get_last_kana(word)
if last_kana in ("", ""):
msg = f"{ctx.author} loses, last letter cannot be ん"
# msg += f"\n{ctx.author}の勝利。最後の仮名を「ん」にすることはできない"
return await self.player_lost(ctx, msg)
# Append the first matched word since we have one
self.used_words.append(word)
self.last_kana = last_kana
self.last_player = ctx.author
await ctx.message.add_reaction("🟢")
return False
class Games(commands.Cog):
def __init__(self):
self.running_games = collections.defaultdict(dict)
@ -20,73 +189,18 @@ class Games(commands.Cog):
The kana cannot be used, as no word in Japanese starts with this
The word used cannot be a previously given word
"""
game = self.running_games["shiritori"].get(ctx.channel.id)
# Ensure only one game is happening at once
if game is not None:
if ctx.author not in game["players"]:
game["players"].append(ctx.author)
else:
self.running_games["shiritori"][ctx.channel.id] = {
"players": [ctx.author],
"words_used": [],
"last_letters": [],
"last_author": ctx.author,
}
game = self.running_games["shiritori"].get(ctx.channel.id)
game = self.running_games["shiritori"].get(ctx.channel.id)
if game is None:
game = Shiritori()
self.running_games["shiritori"][ctx.channel.id] = game
def grab_letter(readings, last=True):
readings = [reversed(word) if last else iter(word) for word in readings]
letters = []
for reading in readings:
for char in reading:
if char.isalpha():
letters.append(char)
break
return letters
# Don't allow last author to guess again
if game["words_used"] and ctx.author == game["last_author"]:
return await ctx.send("You guessed last, someone else's turn")
is_noun, readings = await utils.readings_for_word(word)
# Only nouns can be used
if not is_noun:
self.running_games["shiritori"][ctx.channel.id] = None
return await ctx.send(
f"Game over! {ctx.author} loses, only nouns can be used"
)
# Grab the first letter of this new word and check it
first_letters = grab_letter(readings, last=False)
last_letters = grab_letter(readings, last=False)
# Include extra check for if this is the first word
if game["words_used"]:
# Check if there's a match between first and last letters
if not any(char in first_letters for char in game["last_letters"]):
self.running_games["shiritori"][ctx.channel.id] = None
return await ctx.send(
f"Game over! {ctx.author} loses, first letter of {word} did not match last letter used"
)
# ん cannot be used
if any(char in last_letters for char in ("", "")):
self.running_games["shiritori"][ctx.channel.id] = None
return await ctx.send(
f"Game over! {ctx.author} loses, last letter cannot be ん"
)
# Cannot reuuse words
if word in game["words_used"]:
self.running_games["shiritori"][ctx.channel.id] = None
return await ctx.send(
f"Game over! {ctx.author} loses, {word} has already been used"
)
# If we're here, then the last letter used was valid, save stuff
game["words_used"].append(word)
game["last_letters"] = last_letters
game["last_author"] = ctx.author
await ctx.message.add_reaction("")
if await game.validate_word(ctx, word):
winner = game.players[0]
msg = f"{winner} wins!"
# msg += f"\n{winner}の負け"
await ctx.send(msg)
del self.running_games["shiritori"][ctx.channel.id]
def setup(bot):

View File

@ -12,29 +12,6 @@ def channel_is_nsfw(channel):
return isinstance(channel, discord.DMChannel) or channel.is_nsfw()
async def readings_for_word(word):
"""Returns a tuple, the first element representing if this word is a noun or not,
the second being all the readings for this word"""
data = await request(
"https://jisho.org/api/v1/search/words", payload={"keyword": word}
)
is_noun = False
readings = []
for piece in data["data"]:
# Jisho returns parts of words too, we don't want those for this use case
if piece["slug"] != word:
continue
# Provide the readings for this word
readings.extend([r["reading"] for r in piece["japanese"] if r["word"] == word])
for sense in piece["senses"]:
if "Noun" in sense["parts_of_speech"]:
is_noun = True
return is_noun, readings
async def download_image(url):
"""Returns a file-like object based on the URL provided"""
# Simply read the image, to get the bytes