1
0
Fork 0
mirror of synced 2024-04-29 10:12:29 +12:00
Bonfire/cogs/games.py
2021-03-31 23:27:10 -08:00

209 lines
7.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
@commands.command(aliases=["word_chain", "しりとり", "シリトリ"])
@commands.max_concurrency(1, per=commands.BucketType.channel)
@utils.checks.can_run(send_messages=True)
async def shiritori(self, ctx, *, word):
"""
Starts or play on a game of Shiritori, in which the last letter of the last word given
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
The kana ん cannot be used, as no word in Japanese starts with this
The word used cannot be a previously given word
"""
# Ensure only one game is happening at once
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]
def setup(bot):
bot.add_cog(Games())