diff --git a/.dockerignore b/.dockerignore index 8fce603..e89bf3b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,2 @@ data/ +.venv/ diff --git a/locales/strings.en-US.json b/locales/strings.en-US.json index fdc0132..9a192c3 100644 --- a/locales/strings.en-US.json +++ b/locales/strings.en-US.json @@ -78,6 +78,18 @@ "case_type_field": "Type:", "case_type_field_value": "`{0}`", "case_type_field_value_with_duration": "`{0} ({1})`", + "coinflip_correct_prediction_author": "Correct Prediction!", + "coinflip_correct_prediction_description": "the coin landed on **{0}**. You predicted correctly!", + "coinflip_flipping_animation_1": "\ud83e\ude99 Flipping...", + "coinflip_flipping_animation_2": "\ud83e\ude99 Flipping..", + "coinflip_flipping_animation_3": "\ud83e\ude99 Flipping.", + "coinflip_flipping_author": "flipping a Coin", + "coinflip_flipping_description": "the coin is in the air...", + "coinflip_invalid_prediction_description": "please enter a valid prediction ('heads'/'h' or 'tails'/'t').", + "coinflip_result_author": "Coin Flip Result", + "coinflip_result_description": "the coin landed on **{0}**.", + "coinflip_wrong_prediction_author": "Wrong Prediction", + "coinflip_wrong_prediction_description": "the coin landed on **{0}**. Your prediction was incorrect.", "config_author": "Server Configuration", "config_birthday_channel_set": "birthday announcements will be sent in {0}.", "config_birthday_module_already_disabled": "the birthday module was already disabled.", @@ -150,6 +162,7 @@ "error_actionable_hierarchy_bot": "I don't have permission to perform this action on this user due to role hierarchy.", "error_actionable_hierarchy_user": "you don't have permission to perform this action on this user due to role hierarchy.", "error_actionable_self": "you can't perform this action on yourself.", + "error_already_flipping_coin_description": "you already have a coinflip running.", "error_already_playing_blackjack": "you already have a game of blackjack running.", "error_birthdays_disabled_author": "Birthdays Disabled", "error_birthdays_disabled_description": "birthdays are disabled in this server.", diff --git a/modules/economy/coinflip.py b/modules/economy/coinflip.py new file mode 100644 index 0000000..0665fb4 --- /dev/null +++ b/modules/economy/coinflip.py @@ -0,0 +1,182 @@ +import asyncio +import random + +import discord +from discord import app_commands +from discord.ext import commands + +import lib.format +from lib.client import Luminara +from lib.const import CONST +from lib.exceptions import LumiException +from ui.embeds import Builder + +PREDICTION_MAPPING = { + "h": "heads", + "head": "heads", + "heads": "heads", + "t": "tails", + "tail": "tails", + "tails": "tails", +} + +COIN_FLIP_DELAY = 0.5 # seconds + + +class ActiveCoinflips: + def __init__(self): + self._flips: set[int] = set() + + def add(self, user_id: int) -> bool: + if user_id in self._flips: + return False + self._flips.add(user_id) + return True + + def remove(self, user_id: int) -> None: + self._flips.discard(user_id) + + +class Coinflip(commands.Cog): + def __init__(self, bot: Luminara) -> None: + self.bot: Luminara = bot + self.coinflip.usage = lib.format.generate_usage(self.coinflip) + self.active_coinflips = ActiveCoinflips() + + @commands.command( + name="coinflip", + aliases=["cf"], + ) + @commands.guild_only() + async def coinflip( + self, + ctx: commands.Context[Luminara], + *, + prediction: str | None = None, + ) -> None: + """ + Flip a coin. Optionally predict the outcome. + + Parameters + ---------- + ctx : commands.Context[Luminara] + The context of the command. + prediction : str, optional + The predicted outcome ('heads', 'h', 'tails', or 't'). + """ + await self._coinflip(ctx, prediction) + + @app_commands.command(name="coinflip", description="Flip a coin. Optionally predict the outcome.") + @app_commands.guild_only() + @app_commands.choices( + prediction=[ + app_commands.Choice(name="Heads", value="heads"), + app_commands.Choice(name="Tails", value="tails"), + ], + ) + async def coinflip_slash( + self, + interaction: discord.Interaction, + prediction: app_commands.Choice[str] | None = None, + ) -> None: + """ + Flip a coin. Optionally predict the outcome. + + Parameters + ---------- + interaction : discord.Interaction + The interaction of the command. + prediction : app_commands.Choice[str], optional + The predicted outcome ('heads' or 'tails'). + """ + await self._coinflip(interaction, prediction.value if prediction else None) + + async def _coinflip(self, ctx: commands.Context[Luminara] | discord.Interaction, prediction: str | None) -> None: + if isinstance(ctx, commands.Context): + author = ctx.author + reply = ctx.reply + else: + author = ctx.user + reply = ctx.followup.send + + if not self.active_coinflips.add(author.id): + raise LumiException(CONST.STRINGS["error_already_flipping_coin_description"]) + + try: + result = random.choice(["heads", "tails"]) + + if prediction: + prediction = PREDICTION_MAPPING.get(prediction.lower()) + if not prediction: + raise LumiException(CONST.STRINGS["coinflip_invalid_prediction_description"]) + + flip_embed = Builder.create_embed( + Builder.INFO, + user_name=author.name, + author_text=CONST.STRINGS["coinflip_flipping_author"], + description=CONST.STRINGS["coinflip_flipping_description"], + ) + + if isinstance(ctx, commands.Context): + flip_message = await reply(embed=flip_embed) + else: + await ctx.response.send_message(embed=flip_embed) + flip_message = await ctx.original_response() + + # Add a short delay before starting the coin flip animation + await asyncio.sleep(COIN_FLIP_DELAY) + + if isinstance(flip_message, discord.Message): + await self._animate_coin_flip(flip_message, author.name) + + if prediction: + predicted_correctly = prediction == result + + embed_type = Builder.SUCCESS if predicted_correctly else Builder.ERROR + author_text = CONST.STRINGS[ + "coinflip_correct_prediction_author" if predicted_correctly else "coinflip_wrong_prediction_author" + ] + description = CONST.STRINGS[ + "coinflip_correct_prediction_description" + if predicted_correctly + else "coinflip_wrong_prediction_description" + ].format(result) + + embed = Builder.create_embed( + embed_type, + user_name=author.name, + author_text=author_text, + description=description, + ) + else: + embed = Builder.create_embed( + Builder.INFO, + user_name=author.name, + author_text=CONST.STRINGS["coinflip_result_author"], + description=CONST.STRINGS["coinflip_result_description"].format(result), + ) + + if flip_message is not None: + await flip_message.edit(embed=embed) + finally: + self.active_coinflips.remove(author.id) + + async def _animate_coin_flip(self, flip_message: discord.Message, author_name: str) -> None: + animations = [ + CONST.STRINGS["coinflip_flipping_animation_1"], + CONST.STRINGS["coinflip_flipping_animation_2"], + CONST.STRINGS["coinflip_flipping_animation_3"], + ] + for animation in animations: + flip_embed = Builder.create_embed( + Builder.INFO, + user_name=author_name, + author_text=CONST.STRINGS["coinflip_flipping_author"], + description=animation, + ) + await flip_message.edit(embed=flip_embed) + await asyncio.sleep(COIN_FLIP_DELAY) + + +async def setup(bot: Luminara) -> None: + await bot.add_cog(Coinflip(bot)) diff --git a/settings.yaml b/settings.yaml index 9af9460..199a017 100644 --- a/settings.yaml +++ b/settings.yaml @@ -88,7 +88,7 @@ info: title: Luminara author: wlinator license: GNU General Public License v3.0 - version: "3.0.1" + version: "3.1.0" repository_url: https://git.wlinator.org/Luminara/Lumi invite_url: https://discord.com/oauth2/authorize?client_id=1038050427272429588&permissions=8&scope=bot