Move shiritori into a class as it was getting complicated
This commit is contained in:
parent
f048e532e0
commit
e050b3ba77
244
cogs/games.py
244
cogs/games.py
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue