2021-03-27 09:33:23 +13:00
|
|
|
|
from dataclasses import dataclass
|
2020-08-05 06:56:27 +12:00
|
|
|
|
from discord.ext import commands
|
|
|
|
|
|
2020-11-08 10:22:24 +13:00
|
|
|
|
import collections
|
2021-03-26 13:03:58 +13:00
|
|
|
|
import utils
|
2020-08-05 06:56:27 +12:00
|
|
|
|
|
|
|
|
|
|
2021-03-27 09:33:23 +13:00
|
|
|
|
@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
|
|
|
|
|
|
|
|
|
|
|
2020-08-05 06:56:27 +12:00
|
|
|
|
class Games(commands.Cog):
|
2020-11-08 10:22:24 +13:00
|
|
|
|
def __init__(self):
|
|
|
|
|
self.running_games = collections.defaultdict(dict)
|
|
|
|
|
|
2021-03-26 13:03:58 +13:00
|
|
|
|
@commands.command(aliases=["word_chain", "しりとり", "シリトリ"])
|
|
|
|
|
@commands.max_concurrency(1, per=commands.BucketType.channel)
|
2021-04-01 20:27:10 +13:00
|
|
|
|
@utils.checks.can_run(send_messages=True)
|
2021-03-26 13:52:53 +13:00
|
|
|
|
async def shiritori(self, ctx, *, word):
|
2020-08-05 06:56:27 +12:00
|
|
|
|
"""
|
2021-03-26 13:52:53 +13:00
|
|
|
|
Starts or play on a game of Shiritori, in which the last letter of the last word given
|
2020-08-05 06:56:27 +12:00
|
|
|
|
has to be the first letter of the next word given. For example, if the word given is
|
|
|
|
|
apple, then the next word can be elephant because apple ends in e and elephant begins in e
|
|
|
|
|
|
|
|
|
|
The last player who entered a word cannot be the next person who enters a word
|
2020-08-05 07:23:38 +12:00
|
|
|
|
The kana ん cannot be used, as no word in Japanese starts with this
|
|
|
|
|
The word used cannot be a previously given word
|
2020-08-05 06:56:27 +12:00
|
|
|
|
"""
|
2021-03-26 13:52:53 +13:00
|
|
|
|
# Ensure only one game is happening at once
|
2021-03-27 09:33:23 +13:00
|
|
|
|
game = self.running_games["shiritori"].get(ctx.channel.id)
|
|
|
|
|
if game is None:
|
|
|
|
|
game = Shiritori()
|
|
|
|
|
self.running_games["shiritori"][ctx.channel.id] = game
|
|
|
|
|
|
|
|
|
|
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]
|
2020-08-05 06:56:27 +12:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def setup(bot):
|
|
|
|
|
bot.add_cog(Games())
|