From 5c9f2290190767d93cedbde56992a81137081cd4 Mon Sep 17 00:00:00 2001 From: wlinator Date: Sun, 22 Sep 2024 16:15:14 +0200 Subject: [PATCH 1/5] feat: Add coinflip command with optional prediction --- .dockerignore | 1 + locales/strings.en-US.json | 13 +++++ modules/economy/coinflip.py | 102 ++++++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 modules/economy/coinflip.py 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..3ba7ffd 100644 --- a/locales/strings.en-US.json +++ b/locales/strings.en-US.json @@ -78,6 +78,19 @@ "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\ude79 Flipping...", + "coinflip_flipping_animation_2": "\ud83e\ude79 Flipping..", + "coinflip_flipping_animation_3": "\ud83e\ude79 Flipping.", + "coinflip_flipping_author": "Flipping a Coin", + "coinflip_flipping_description": "The coin is in the air...", + "coinflip_invalid_prediction_author": "Invalid Prediction", + "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.", diff --git a/modules/economy/coinflip.py b/modules/economy/coinflip.py new file mode 100644 index 0000000..7dd09a3 --- /dev/null +++ b/modules/economy/coinflip.py @@ -0,0 +1,102 @@ +import asyncio +import random + +from discord.ext import commands + +import lib.format +from lib.client import Luminara +from lib.const import CONST +from ui.embeds import Builder + + +class Coinflip(commands.Cog): + def __init__(self, bot: Luminara) -> None: + self.bot: Luminara = bot + self.coinflip.usage = lib.format.generate_usage(self.coinflip) + + @commands.hybrid_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' or 'tails'/'t'). + """ + result = random.choice(["heads", "tails"]) + + flip_embed = Builder.create_embed( + Builder.INFO, + user_name=ctx.author.name, + author_text=CONST.STRINGS["coinflip_flipping_author"], + description=CONST.STRINGS["coinflip_flipping_description"], + ) + flip_message = await ctx.send(embed=flip_embed) + + await asyncio.sleep(1) + + for animation in ( + CONST.STRINGS["coinflip_flipping_animation_1"], + CONST.STRINGS["coinflip_flipping_animation_2"], + CONST.STRINGS["coinflip_flipping_animation_3"], + ): + flip_embed = Builder.create_embed( + Builder.INFO, + user_name=ctx.author.name, + author_text=CONST.STRINGS["coinflip_flipping_author"], + description=animation, + ) + await flip_message.edit(embed=flip_embed) + await asyncio.sleep(0.5) + + if prediction: + prediction = prediction.lower() + if prediction in ["heads", "h", "tails", "t"]: + predicted_correctly = (prediction.startswith("h") and result == "heads") or ( + prediction.startswith("t") and result == "tails" + ) + if predicted_correctly: + embed = Builder.create_embed( + Builder.SUCCESS, + user_name=ctx.author.name, + author_text=CONST.STRINGS["coinflip_correct_prediction_author"], + description=CONST.STRINGS["coinflip_correct_prediction_description"].format(result), + ) + else: + embed = Builder.create_embed( + Builder.ERROR, + user_name=ctx.author.name, + author_text=CONST.STRINGS["coinflip_wrong_prediction_author"], + description=CONST.STRINGS["coinflip_wrong_prediction_description"].format(result), + ) + else: + embed = Builder.create_embed( + Builder.ERROR, + user_name=ctx.author.name, + author_text=CONST.STRINGS["coinflip_invalid_prediction_author"], + description=CONST.STRINGS["coinflip_invalid_prediction_description"], + ) + else: + embed = Builder.create_embed( + Builder.INFO, + user_name=ctx.author.name, + author_text=CONST.STRINGS["coinflip_result_author"], + description=CONST.STRINGS["coinflip_result_description"].format(result), + ) + + await flip_message.edit(embed=embed) + + +async def setup(bot: Luminara) -> None: + await bot.add_cog(Coinflip(bot)) From 360454c152e527d0b3145b2695cbaa336f65594f Mon Sep 17 00:00:00 2001 From: wlinator Date: Sun, 22 Sep 2024 16:22:45 +0200 Subject: [PATCH 2/5] refactor: Update coinflip command to handle invalid predictions --- locales/strings.en-US.json | 18 ++++++++-------- modules/economy/coinflip.py | 43 ++++++++++++++++++++----------------- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/locales/strings.en-US.json b/locales/strings.en-US.json index 3ba7ffd..9cb66bb 100644 --- a/locales/strings.en-US.json +++ b/locales/strings.en-US.json @@ -79,18 +79,18 @@ "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\ude79 Flipping...", - "coinflip_flipping_animation_2": "\ud83e\ude79 Flipping..", - "coinflip_flipping_animation_3": "\ud83e\ude79 Flipping.", - "coinflip_flipping_author": "Flipping a Coin", - "coinflip_flipping_description": "The coin is in the air...", + "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_author": "Invalid Prediction", - "coinflip_invalid_prediction_description": "Please enter a valid prediction ('heads'/'h' or 'tails'/'t').", + "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_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.", + "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.", diff --git a/modules/economy/coinflip.py b/modules/economy/coinflip.py index 7dd09a3..4dc2eb5 100644 --- a/modules/economy/coinflip.py +++ b/modules/economy/coinflip.py @@ -36,6 +36,18 @@ class Coinflip(commands.Cog): """ result = random.choice(["heads", "tails"]) + if prediction: + prediction = prediction.lower() + if prediction not in ["heads", "h", "tails", "t"]: + embed = Builder.create_embed( + Builder.ERROR, + user_name=ctx.author.name, + author_text=CONST.STRINGS["coinflip_invalid_prediction_author"], + description=CONST.STRINGS["coinflip_invalid_prediction_description"], + ) + await ctx.send(embed=embed) + return + flip_embed = Builder.create_embed( Builder.INFO, user_name=ctx.author.name, @@ -61,31 +73,22 @@ class Coinflip(commands.Cog): await asyncio.sleep(0.5) if prediction: - prediction = prediction.lower() - if prediction in ["heads", "h", "tails", "t"]: - predicted_correctly = (prediction.startswith("h") and result == "heads") or ( - prediction.startswith("t") and result == "tails" + predicted_correctly = (prediction.startswith("h") and result == "heads") or ( + prediction.startswith("t") and result == "tails" + ) + if predicted_correctly: + embed = Builder.create_embed( + Builder.SUCCESS, + user_name=ctx.author.name, + author_text=CONST.STRINGS["coinflip_correct_prediction_author"], + description=CONST.STRINGS["coinflip_correct_prediction_description"].format(result), ) - if predicted_correctly: - embed = Builder.create_embed( - Builder.SUCCESS, - user_name=ctx.author.name, - author_text=CONST.STRINGS["coinflip_correct_prediction_author"], - description=CONST.STRINGS["coinflip_correct_prediction_description"].format(result), - ) - else: - embed = Builder.create_embed( - Builder.ERROR, - user_name=ctx.author.name, - author_text=CONST.STRINGS["coinflip_wrong_prediction_author"], - description=CONST.STRINGS["coinflip_wrong_prediction_description"].format(result), - ) else: embed = Builder.create_embed( Builder.ERROR, user_name=ctx.author.name, - author_text=CONST.STRINGS["coinflip_invalid_prediction_author"], - description=CONST.STRINGS["coinflip_invalid_prediction_description"], + author_text=CONST.STRINGS["coinflip_wrong_prediction_author"], + description=CONST.STRINGS["coinflip_wrong_prediction_description"].format(result), ) else: embed = Builder.create_embed( From b8139a9432fc3235c62b8038f5f33967ee2a3297 Mon Sep 17 00:00:00 2001 From: wlinator Date: Sun, 22 Sep 2024 16:45:26 +0200 Subject: [PATCH 3/5] refactor: Optimize coinflip command --- locales/strings.en-US.json | 2 +- modules/economy/coinflip.py | 155 ++++++++++++++++++++++++------------ 2 files changed, 105 insertions(+), 52 deletions(-) diff --git a/locales/strings.en-US.json b/locales/strings.en-US.json index 9cb66bb..9a192c3 100644 --- a/locales/strings.en-US.json +++ b/locales/strings.en-US.json @@ -85,7 +85,6 @@ "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_author": "Invalid Prediction", "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}**.", @@ -163,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 index 4dc2eb5..767c2d4 100644 --- a/modules/economy/coinflip.py +++ b/modules/economy/coinflip.py @@ -1,20 +1,25 @@ 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 +ACTIVE_COINFLIPS: dict[int, bool] = {} + class Coinflip(commands.Cog): def __init__(self, bot: Luminara) -> None: self.bot: Luminara = bot self.coinflip.usage = lib.format.generate_usage(self.coinflip) - @commands.hybrid_command( + @commands.command( name="coinflip", aliases=["cf"], ) @@ -22,6 +27,7 @@ class Coinflip(commands.Cog): async def coinflip( self, ctx: commands.Context[Luminara], + *, prediction: str | None = None, ) -> None: """ @@ -32,73 +38,120 @@ class Coinflip(commands.Cog): ctx : commands.Context[Luminara] The context of the command. prediction : str, optional - The predicted outcome ('heads'/'h' or 'tails'/'t'). + The predicted outcome ('heads', 'h', 'tails', or 't'). """ - result = random.choice(["heads", "tails"]) - if prediction: prediction = prediction.lower() - if prediction not in ["heads", "h", "tails", "t"]: - embed = Builder.create_embed( - Builder.ERROR, - user_name=ctx.author.name, - author_text=CONST.STRINGS["coinflip_invalid_prediction_author"], - description=CONST.STRINGS["coinflip_invalid_prediction_description"], - ) - await ctx.send(embed=embed) - return + if prediction in ["h", "head"]: + prediction = "heads" + elif prediction in ["t", "tail"]: + prediction = "tails" - flip_embed = Builder.create_embed( - Builder.INFO, - user_name=ctx.author.name, - author_text=CONST.STRINGS["coinflip_flipping_author"], - description=CONST.STRINGS["coinflip_flipping_description"], - ) - flip_message = await ctx.send(embed=flip_embed) + await self._coinflip(ctx, prediction) - await asyncio.sleep(1) + @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 author.id in ACTIVE_COINFLIPS: + raise LumiException(CONST.STRINGS["error_already_flipping_coin_description"]) + + ACTIVE_COINFLIPS[author.id] = True + + try: + result = random.choice(["heads", "tails"]) + + if prediction and prediction not in ["heads", "tails"]: + raise LumiException(CONST.STRINGS["coinflip_invalid_prediction_description"]) - for animation in ( - CONST.STRINGS["coinflip_flipping_animation_1"], - CONST.STRINGS["coinflip_flipping_animation_2"], - CONST.STRINGS["coinflip_flipping_animation_3"], - ): flip_embed = Builder.create_embed( Builder.INFO, - user_name=ctx.author.name, + user_name=author.name, author_text=CONST.STRINGS["coinflip_flipping_author"], - description=animation, + description=CONST.STRINGS["coinflip_flipping_description"], ) - await flip_message.edit(embed=flip_embed) - await asyncio.sleep(0.5) - if prediction: - predicted_correctly = (prediction.startswith("h") and result == "heads") or ( - prediction.startswith("t") and result == "tails" - ) - if predicted_correctly: + 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() + + await asyncio.sleep(1) + + if flip_message is not None: + flip_embed.description = f"**{author.name}** " + CONST.STRINGS["coinflip_flipping_animation_1"] + await flip_message.edit(embed=flip_embed) + await asyncio.sleep(0.5) + + flip_embed.description = f"**{author.name}** " + CONST.STRINGS["coinflip_flipping_animation_2"] + await flip_message.edit(embed=flip_embed) + await asyncio.sleep(0.5) + + flip_embed.description = f"**{author.name}** " + CONST.STRINGS["coinflip_flipping_animation_3"] + await flip_message.edit(embed=flip_embed) + await asyncio.sleep(0.5) + + 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( - Builder.SUCCESS, - user_name=ctx.author.name, - author_text=CONST.STRINGS["coinflip_correct_prediction_author"], - description=CONST.STRINGS["coinflip_correct_prediction_description"].format(result), + embed_type, + user_name=author.name, + author_text=author_text, + description=description, ) else: embed = Builder.create_embed( - Builder.ERROR, - user_name=ctx.author.name, - author_text=CONST.STRINGS["coinflip_wrong_prediction_author"], - description=CONST.STRINGS["coinflip_wrong_prediction_description"].format(result), + Builder.INFO, + user_name=author.name, + author_text=CONST.STRINGS["coinflip_result_author"], + description=CONST.STRINGS["coinflip_result_description"].format(result), ) - else: - embed = Builder.create_embed( - Builder.INFO, - user_name=ctx.author.name, - author_text=CONST.STRINGS["coinflip_result_author"], - description=CONST.STRINGS["coinflip_result_description"].format(result), - ) - await flip_message.edit(embed=embed) + if flip_message is not None: + await flip_message.edit(embed=embed) + finally: + del ACTIVE_COINFLIPS[author.id] async def setup(bot: Luminara) -> None: From fd6e80d96afbf8ab187a27cb2f656ebda2314184 Mon Sep 17 00:00:00 2001 From: wlinator Date: Sun, 22 Sep 2024 16:48:27 +0200 Subject: [PATCH 4/5] up version --- settings.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From c8ba9174ed0624de24d1c3cbf064e9e1c26e2056 Mon Sep 17 00:00:00 2001 From: wlinator Date: Sun, 22 Sep 2024 16:59:31 +0200 Subject: [PATCH 5/5] refactor: Optimize cf --- modules/economy/coinflip.py | 78 ++++++++++++++++++++++++------------- 1 file changed, 51 insertions(+), 27 deletions(-) diff --git a/modules/economy/coinflip.py b/modules/economy/coinflip.py index 767c2d4..0665fb4 100644 --- a/modules/economy/coinflip.py +++ b/modules/economy/coinflip.py @@ -11,13 +11,37 @@ from lib.const import CONST from lib.exceptions import LumiException from ui.embeds import Builder -ACTIVE_COINFLIPS: dict[int, bool] = {} +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", @@ -40,13 +64,6 @@ class Coinflip(commands.Cog): prediction : str, optional The predicted outcome ('heads', 'h', 'tails', or 't'). """ - if prediction: - prediction = prediction.lower() - if prediction in ["h", "head"]: - prediction = "heads" - elif prediction in ["t", "tail"]: - prediction = "tails" - await self._coinflip(ctx, prediction) @app_commands.command(name="coinflip", description="Flip a coin. Optionally predict the outcome.") @@ -82,16 +99,16 @@ class Coinflip(commands.Cog): author = ctx.user reply = ctx.followup.send - if author.id in ACTIVE_COINFLIPS: + if not self.active_coinflips.add(author.id): raise LumiException(CONST.STRINGS["error_already_flipping_coin_description"]) - ACTIVE_COINFLIPS[author.id] = True - try: result = random.choice(["heads", "tails"]) - if prediction and prediction not in ["heads", "tails"]: - raise LumiException(CONST.STRINGS["coinflip_invalid_prediction_description"]) + 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, @@ -106,20 +123,11 @@ class Coinflip(commands.Cog): await ctx.response.send_message(embed=flip_embed) flip_message = await ctx.original_response() - await asyncio.sleep(1) + # Add a short delay before starting the coin flip animation + await asyncio.sleep(COIN_FLIP_DELAY) - if flip_message is not None: - flip_embed.description = f"**{author.name}** " + CONST.STRINGS["coinflip_flipping_animation_1"] - await flip_message.edit(embed=flip_embed) - await asyncio.sleep(0.5) - - flip_embed.description = f"**{author.name}** " + CONST.STRINGS["coinflip_flipping_animation_2"] - await flip_message.edit(embed=flip_embed) - await asyncio.sleep(0.5) - - flip_embed.description = f"**{author.name}** " + CONST.STRINGS["coinflip_flipping_animation_3"] - await flip_message.edit(embed=flip_embed) - await asyncio.sleep(0.5) + if isinstance(flip_message, discord.Message): + await self._animate_coin_flip(flip_message, author.name) if prediction: predicted_correctly = prediction == result @@ -151,7 +159,23 @@ class Coinflip(commands.Cog): if flip_message is not None: await flip_message.edit(embed=embed) finally: - del ACTIVE_COINFLIPS[author.id] + 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: