From ce23128e922b29543d9bb1cde564b6d8596a6a1d Mon Sep 17 00:00:00 2001 From: youngcoder45 Date: Sun, 5 Apr 2026 16:18:54 +0530 Subject: [PATCH 1/3] remove unused cogs --- cogs/rockpaperscissors.py | 332 -------------------------------------- 1 file changed, 332 deletions(-) delete mode 100644 cogs/rockpaperscissors.py diff --git a/cogs/rockpaperscissors.py b/cogs/rockpaperscissors.py deleted file mode 100644 index 487bf3c..0000000 --- a/cogs/rockpaperscissors.py +++ /dev/null @@ -1,332 +0,0 @@ -import discord -from discord.ext import commands -from discord.ui import Button, View -from discord import app_commands -import asyncio -import time -import random -from typing import Optional, Union - -EMOJIS = { - "rock": "🪨", - "paper": "📄", - "scissors": "✂️" -} -CHECK_MARK = "✅" -EMPTY_MARK = "⠀⠀" # Gleich breit wie CHECK_MARK -SKULL = "☠" -HEART = "❤️" - -class RockPaperScissorsGame: - def __init__(self, player1, player2, ai_mode=False): - self.players = [player1, player2] - self.lives = [3, 3] - self.choices = [None, None] - self.rounds = [] - self.game_over = False - self.winner = None - self.ai_mode = ai_mode - - def set_choice(self, player_index, choice): - if self.choices[player_index] is not None: - return False - self.choices[player_index] = choice - return True - - def both_chosen(self): - return self.choices[0] is not None and self.choices[1] is not None - - def determine_winner_of_round(self): - p1 = self.choices[0] - p2 = self.choices[1] - if p1 is None or p2 is None: - return -1 # Draw if either choice is None - if p1 == p2: - return -1 # Draw - wins = { - "rock": "scissors", - "scissors": "paper", - "paper": "rock" - } - if wins[p1] == p2: - return 0 - else: - return 1 - - def end_round(self): - winner = self.determine_winner_of_round() - self.rounds.append((self.choices[0], self.choices[1], winner)) - if winner != -1: - loser = 1 - winner - self.lives[loser] -= 1 - self.choices = [None, None] - - if self.lives[0] == 0: - self.game_over = True - self.winner = 1 - elif self.lives[1] == 0: - self.game_over = True - self.winner = 0 - -class RockPaperScissorsView(View): - def __init__(self, game, interaction, cog): - super().__init__(timeout=30) - self.game = game - self.interaction = interaction - self.cog = cog - self.timeout_task = None - self.timeout_until = int(time.time()) + 30 # Zeitstempel für Timeout - - for choice in ["rock", "paper", "scissors"]: - btn = Button(label="", emoji=EMOJIS[choice], style=discord.ButtonStyle.primary, custom_id=choice) - btn.callback = self.make_choice_callback(choice) - self.add_item(btn) - - def make_choice_callback(self, choice): - async def callback(interaction: discord.Interaction): - if self.game.game_over: - return - - player_index = None - for idx, player in enumerate(self.game.players): - if player.id == interaction.user.id: - player_index = idx - break - if player_index is None: - await interaction.response.defer() - return - - if self.game.choices[player_index] is not None: - await interaction.response.defer() - return - - self.game.set_choice(player_index, choice) - await interaction.response.defer() - - # Timeout NICHT zurücksetzen, sondern erst nach beiden Zügen - - # Wenn gegen Bot: Bot wählt sofort nach dem Spieler - if self.game.ai_mode and player_index == 0 and not self.game.choices[1]: - await asyncio.sleep(0.5) - self.game.choices[1] = self.bot_choice() - await self.update_message() - - if self.game.both_chosen(): - self.game.end_round() - # Timeout jetzt zurücksetzen! - if self.timeout_task and not self.timeout_task.done(): - self.timeout_task.cancel() - self.timeout_until = int(time.time()) + 30 - self.timeout_task = asyncio.create_task(self.player_timeout()) - - await self.update_message() - - if self.game.game_over: - self.clear_items() - await self.update_message() - self.cog.active_players.discard(self.game.players[0].id) - if not self.game.ai_mode: - self.cog.active_players.discard(self.game.players[1].id) - if self.timeout_task and not self.timeout_task.done(): - self.timeout_task.cancel() - - return callback - - async def player_timeout(self): - await asyncio.sleep(30) - if not self.game.game_over: - self.clear_items() - await self.interaction.edit_original_response(content=self.format_message(cancelled=True), view=self) - self.cog.active_players.discard(self.game.players[0].id) - if not self.game.ai_mode: - self.cog.active_players.discard(self.game.players[1].id) - - def bot_choice(self): - return random.choice(["rock", "paper", "scissors"]) - - def format_lives(self, player_index): - lives = self.game.lives[player_index] - return HEART * lives if lives > 0 else SKULL - - def format_player_line(self, player_index): - mark = CHECK_MARK if self.game.choices[player_index] is not None else EMPTY_MARK - player = self.game.players[player_index] - lives = self.format_lives(player_index) - return f"⠀ {mark} {player.mention} {lives}" - - def format_player_line_endgame(self, player_index): - player = self.game.players[player_index] - lives = self.format_lives(player_index) - return f"{player.display_name} {lives}" - - def format_rounds(self): - if not self.game.rounds: - return "" - lines = [] - for p1_choice, p2_choice, winner in self.game.rounds: - if winner == 0: - winner_name = f" [{self.game.players[0].display_name}]" - elif winner == 1: - winner_name = f" [{self.game.players[1].display_name}]" - else: - winner_name = " [Draw]" - lines.append(f"``{EMOJIS[p1_choice]} vs {EMOJIS[p2_choice]}{winner_name}``") - return "\n".join(lines) - - def format_message(self, cancelled=False): - header = f"**Rock Paper Scissors [**{self.game.players[0].display_name} vs {self.game.players[1].display_name}**]**" - rounds = self.format_rounds() - # Timer immer anzeigen, außer bei Game Over/Timeout - timer_line = "" - if not cancelled and not self.game.game_over: - timer_line = f"\nTimeout " - - if cancelled: - body = "\n**Timeout.**" - body += f"\n\n{self.format_player_line_endgame(0)}\n{self.format_player_line_endgame(1)}" - if rounds: - body += f"\n\n{rounds}" - elif self.game.game_over: - winner_id = self.game.winner - if winner_id is not None: - winner = self.game.players[winner_id] - body = f"\n{winner.mention} has **won!**\n\n" - else: - body = "Draw!\n\n" - body += f"{self.format_player_line_endgame(0)}\n{self.format_player_line_endgame(1)}" - if rounds: - body += f"\n\n{rounds}" - else: - if rounds: - body = f"\n\n{self.format_player_line(0)}\n{self.format_player_line(1)}\n\n{rounds}" - else: - body = f"\n\n{self.format_player_line(0)}\n{self.format_player_line(1)}\n\n⠀" - - # Timer über das Spielfeld, unter dem Header - return f"{header}{timer_line}{body}" - - async def on_timeout(self): - if not self.game.game_over: - self.clear_items() - await self.interaction.edit_original_response(content=self.format_message(cancelled=True), view=self) - self.cog.active_players.discard(self.game.players[0].id) - if not self.game.ai_mode: - self.cog.active_players.discard(self.game.players[1].id) - - async def update_message(self): - await self.interaction.edit_original_response(content=self.format_message(), view=self) - -class RockPaperScissorsChallengeView(View): - def __init__(self, challenger, opponent, cog, message): - super().__init__(timeout=31) - self.challenger = challenger - self.opponent = opponent - self.cog = cog - # For slash command flows, the underlying message is an InteractionMessage. - # We set it later once the original response is available. - self.message: Optional[Union[discord.Message, discord.InteractionMessage]] = message - - @discord.ui.button(label="Accept", style=discord.ButtonStyle.green) - async def accept_button(self, interaction: discord.Interaction, button: Button): - if interaction.user.id != self.opponent.id: - embed = discord.Embed( - description=f"Only {self.opponent.mention} can accept this challenge.", - colour=discord.Colour.red() - ) - await interaction.response.send_message(embed=embed, ephemeral=True) - return - - await interaction.response.defer() - button.disabled = True - - # If the view was created before we had the original response, fall back to the - # message that triggered this component interaction. - if self.message is None and interaction.message is not None: - self.message = interaction.message - - game = RockPaperScissorsGame(self.challenger, self.opponent) - game_view = RockPaperScissorsView(game, interaction, self.cog) - - if self.message is not None and self.message.id in self.cog.challenges: - self.cog.challenges[self.message.id]["accepted"] = True - - if self.message is not None: - await self.message.edit( - content=game_view.format_message(), - view=game_view - ) - -class RockPaperScissorsCog(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.challenges = {} - self.active_players = set() - - @app_commands.command(name="rockpaperscissors", description="Challenge another player or the bot to Rock Paper Scissors") - async def rockpaperscissors(self, interaction: discord.Interaction, opponent: discord.User): - challenger = interaction.user - - def error_embed(msg): - return discord.Embed(description=msg, colour=discord.Colour.red()) - - if challenger.id in self.active_players: - return await interaction.response.send_message(embed=error_embed("You are already in a game."), ephemeral=True) - if opponent.id in self.active_players and opponent != self.bot.user: - return await interaction.response.send_message(embed=error_embed("That player is already in a game."), ephemeral=True) - if opponent.id == challenger.id: - return await interaction.response.send_message(embed=error_embed("You cannot challenge yourself."), ephemeral=True) - - # --- Bot Mode --- - if opponent == self.bot.user: - try: - bot_member = interaction.guild.get_member(self.bot.user.id) if interaction.guild else self.bot.user - game = RockPaperScissorsGame(challenger, bot_member, ai_mode=True) - view = RockPaperScissorsView(game, interaction, self) - await interaction.response.send_message( - view.format_message(), - view=view - ) - self.active_players.add(challenger.id) # <-- Jetzt erst eintragen! - except Exception as e: - print(f"Error starting RockPaperScissors vs Bot: {e}") - return - - try: - eta_timestamp = int(time.time()) + 31 - challenge_view = RockPaperScissorsChallengeView(challenger, opponent, self, None) - - await interaction.response.send_message( - f"{opponent.mention}, you have been challenged to **Rock Paper Scissors**!\nThe challenge will expire .", - view=challenge_view - ) - msg = await interaction.original_response() - - challenge_view.message = msg - - self.active_players.add(challenger.id) - self.active_players.add(opponent.id) - - self.challenges[msg.id] = { - "challenger": challenger, - "opponent": opponent, - "accepted": False, - "message": msg, - } - - async def challenge_timeout(): - await asyncio.sleep(31) - if msg.id in self.challenges and not self.challenges[msg.id]["accepted"]: - try: - await msg.delete() - except discord.NotFound: - pass - self.active_players.discard(challenger.id) - self.active_players.discard(opponent.id) - del self.challenges[msg.id] - - asyncio.create_task(challenge_timeout()) - except Exception as e: - print(f"Error starting RockPaperScissors challenge: {e}") - -async def setup(bot): - await bot.add_cog(RockPaperScissorsCog(bot)) From 614d1f0506083b2b0ff7f2d9d48962e95345484d Mon Sep 17 00:00:00 2001 From: youngcoder45 Date: Sun, 5 Apr 2026 16:47:59 +0530 Subject: [PATCH 2/3] feat: major update in Counting --- cogs/codebuddy_quiz.py | 10 +- cogs/counting.py | 258 ++++++++++++---------- cogs/daily_quests.py | 74 ++++--- utils/codebuddy_database.py | 418 +++++++++++++++++++++++++++++------- 4 files changed, 533 insertions(+), 227 deletions(-) diff --git a/cogs/codebuddy_quiz.py b/cogs/codebuddy_quiz.py index 475851a..d0406e5 100644 --- a/cogs/codebuddy_quiz.py +++ b/cogs/codebuddy_quiz.py @@ -133,8 +133,14 @@ async def on_message(self, message: discord.Message): # Notify user about quest completion try: quest_embed = discord.Embed( - title="Daily Quest Completed!", - description=f"{message.author.mention} You completed your daily quest!\n\n**Rewards Earned:**\n• 1 Streak Freeze\n• 1 Bonus Hint\n\nUse `?inventory` to check your rewards!", + title="Quest Completed!", + description=( + f"{message.author.mention} You completed the **Quiz** quest!\n\n" + "**Rewards Earned:**\n" + "• **0.2** Streak Freeze\n" + "• **0.5** Save\n\n" + "Use `?inventory` to check your items!" + ), color=0x000000 ) await message.channel.send(embed=quest_embed) diff --git a/cogs/counting.py b/cogs/counting.py index 1753fde..3426971 100644 --- a/cogs/counting.py +++ b/cogs/counting.py @@ -3,9 +3,16 @@ from discord import app_commands import aiosqlite from utils.codebuddy_database import DB_PATH +from utils.codebuddy_database import ( + add_guild_save_units, + get_guild_save_units, + get_user_save_units, + increment_quest_counting_count, + try_use_guild_save, + try_use_user_save, +) import ast import operator -import random import asyncio import time from typing import Optional @@ -454,6 +461,19 @@ async def on_message(self, message): # Side effects after commit to avoid duplicate reactions on retries. self._enqueue_reaction(message, "✅") + # Daily quest progress: count 5 numbers (best-effort). + try: + quest_completed = await increment_quest_counting_count(message.author.id) + if quest_completed: + await message.channel.send( + f"Daily quest completed, {message.author.mention}! " + "You earned **0.2** Streak Freeze and **0.5** Save. " + "Use `?inventory` to check your items.", + delete_after=15, + ) + except Exception: + pass + # Highscore marker: react ✅+🏆 when reaching/topping the record if next_count >= high_score: await self._mark_highscore_message(message, next_count, high_score) @@ -497,136 +517,136 @@ async def on_message_delete(self, message: discord.Message): ) async def fail_count(self, message, current_count, reason): - # 1. Send initial message - await message.add_reaction("❌") - status_msg = await message.channel.send( - f"{reason} {message.author.mention} messed up at {current_count}!\n" - "🎲 **Rolling the Dice of Fate...**\n" - "React with 🎲 to help roll! (Need 2 reactions in 60s)" - ) - await status_msg.add_reaction("🎲") + # Replace dice mechanic with save mechanic: + # 1) Use a personal save if available. + # 2) Else use a guild save if available. + # 3) Else the count is ruined (reset to 0). - # 2. Wait for reactions - reactions_collected = False try: - end_time = asyncio.get_event_loop().time() + 60 - while True: - # Check current count - status_msg = await message.channel.fetch_message(status_msg.id) - reaction = discord.utils.get(status_msg.reactions, emoji="🎲") - - # If bot reacted, count is at least 1. We need 2 total. - if reaction and reaction.count >= 2: - reactions_collected = True - break - - timeout = end_time - asyncio.get_event_loop().time() - if timeout <= 0: - break - - try: - # Wait for any reaction on this message - await self.bot.wait_for( - 'reaction_add', - check=lambda r, u: r.message.id == status_msg.id and str(r.emoji) == "🎲", - timeout=timeout - ) - except asyncio.TimeoutError: - break + await message.add_reaction("❌") except Exception: - pass # Proceed if something fails + pass - # 3. Determine Outcome - outcome_msg = "" - new_count = 0 - new_last_user_id = None - - dice_db_ops = [] # List of DB operations to perform (query, args) + if not message.guild: + return - if not reactions_collected: - # TIMEOUT / NOT ENOUGH REACTIONS -> RESET - new_count = 0 - new_last_user_id = None - outcome_msg = "⏳ **Time's up!** Not enough people helped roll the dice.\n💥 **Reset!** The count goes back to 0." - - dice_db_ops.append((""" - UPDATE counting_config + guild_id = message.guild.id + user_id = message.author.id + + # Clear this user's warnings so a saved mistake doesn't soft-lock them. + try: + await self._set_warning_count(guild_id, user_id, 0) + except Exception: + pass + + used_personal = False + used_guild = False + try: + used_personal = await try_use_user_save(user_id) + except Exception: + used_personal = False + + if not used_personal: + try: + used_guild = await try_use_guild_save(guild_id) + except Exception: + used_guild = False + + if used_personal or used_guild: + try: + remaining_user_units = await get_user_save_units(user_id) + except Exception: + remaining_user_units = 0 + try: + remaining_guild_units = await get_guild_save_units(guild_id) + except Exception: + remaining_guild_units = 0 + + source = "your" if used_personal else "the server's" + await message.channel.send( + f"{reason} {message.author.mention} messed up at **{current_count}**, " + f"but {source} save was used — the count is **saved**.\n" + f"Next number is **{current_count + 1}**.\n" + f"Your saves: **{remaining_user_units/10:.1f}** • Server saves: **{remaining_guild_units/10:.1f}**" + ) + return + + # No saves: ruin the count (reset to 0) + db_ops = [ + ( + """ + UPDATE counting_config SET current_count = 0, last_user_id = NULL WHERE guild_id = ? - """, (message.guild.id,))) - - dice_db_ops.append((""" + """, + (guild_id,), + ), + ( + """ INSERT INTO counting_stats (user_id, guild_id, total_counts, ruined_counts) VALUES (?, ?, 0, 1) ON CONFLICT(user_id, guild_id) DO UPDATE SET ruined_counts = ruined_counts + 1 - """, (message.author.id, message.guild.id))) + """, + (user_id, guild_id), + ), + ] - else: - # REACTIONS COLLECTED -> ROLL DICE - dice_roll = random.randint(1, 6) - outcome_msg = f"🎲 **Dice Roll: {dice_roll}**\n" - - if dice_roll in [2, 4, 6]: - # SAVE - new_count = current_count - outcome_msg += "✨ **Saved!** The count continues!" - # No update to config needed except maybe verifying it? - # Actually if saved, we do NOTHING to counting_config. - elif dice_roll == 3: - # RESET - new_count = 0 - new_last_user_id = None - outcome_msg += "💥 **Reset!** The count goes back to 0." - elif dice_roll == 1: - # -10 Penalty - new_count = max(0, current_count - 10) - new_last_user_id = None - outcome_msg += "🔻 **-10 Penalty!** The count drops by 10." - elif dice_roll == 5: - # -5 Penalty - new_count = max(0, current_count - 5) - new_last_user_id = None - outcome_msg += "🔻 **-5 Penalty!** The count drops by 5." - - if dice_roll not in [2, 4, 6]: - dice_db_ops.append((""" - UPDATE counting_config - SET current_count = ?, last_user_id = ? - WHERE guild_id = ? - """, (new_count, new_last_user_id, message.guild.id))) - - dice_db_ops.append((""" - INSERT INTO counting_stats (user_id, guild_id, total_counts, ruined_counts) - VALUES (?, ?, 0, 1) - ON CONFLICT(user_id, guild_id) DO UPDATE SET ruined_counts = ruined_counts + 1 - """, (message.author.id, message.guild.id))) + retries = 3 + while retries > 0: + try: + async with aiosqlite.connect(DB_PATH, timeout=30.0) as db: + for sql, args in db_ops: + await db.execute(sql, args) + await db.commit() + break + except aiosqlite.OperationalError as e: + if "locked" in str(e).lower(): + retries -= 1 + await asyncio.sleep(0.3) + continue + break - # EXECUTE DB OPS with Retry - if dice_db_ops: - retries = 3 - while retries > 0: - try: - async with aiosqlite.connect(DB_PATH, timeout=30.0) as db: - for sql, args in dice_db_ops: - await db.execute(sql, args) - await db.commit() - break # Success - except aiosqlite.OperationalError as e: - if "locked" in str(e): - retries -= 1 - await asyncio.sleep(0.5) - else: - print(f"Error saving count fail state: {e}") - break - - # 4. Edit message - await status_msg.edit(content=f"{reason} {message.author.mention} messed up at {current_count}!\n{outcome_msg}\nNext number is **{new_count + 1}**.") - - # If the count was actually changed (ruined/reset/penalty), clear warnings and remove highscore marker. - count_ruined = new_count != current_count - if count_ruined and message.guild and isinstance(message.channel, discord.TextChannel): - await self._clear_all_warnings(message.guild.id) - await self._clear_highscore_marker_if_any(message.guild.id, message.channel) + await message.channel.send( + f"{reason} {message.author.mention} messed up at **{current_count}**. " + "No saves were available — the count is **ruined** and has been reset to **0**.\n" + "Next number is **1**." + ) + + if isinstance(message.channel, discord.TextChannel): + await self._clear_all_warnings(guild_id) + await self._clear_highscore_marker_if_any(guild_id, message.channel) + + + @commands.command(name="donateguild", aliases=["dg"]) + async def donate_guild(self, ctx: commands.Context): + """Donate 1 personal save to the guild pool (guild receives 0.5 save).""" + if not ctx.guild: + return await ctx.send("Server only command.") + + user_id = ctx.author.id + guild_id = ctx.guild.id + + # Need at least 1.0 save (10 units) to donate. + user_units = await get_user_save_units(user_id) + if user_units < 10: + return await ctx.send( + f"You need **1.0** save to donate. Your saves: **{user_units/10:.1f}**" + ) + + # Consume 1.0 personal save + used = await try_use_user_save(user_id) + if not used: + return await ctx.send("Couldn't donate right now (try again).") + + # Guild receives 0.5 save (5 units) + await add_guild_save_units(guild_id, 5) + + new_user_units = await get_user_save_units(user_id) + new_guild_units = await get_guild_save_units(guild_id) + await ctx.send( + f"Donated **1.0** save to the server pool. Server gained **0.5** save.\n" + f"Your saves: **{new_user_units/10:.1f}** • Server saves: **{new_guild_units/10:.1f}**" + ) @commands.hybrid_command(name="highscoretable", aliases=["highscores"], help="Show recent counting highscores") async def highscore_table(self, ctx: commands.Context): diff --git a/cogs/daily_quests.py b/cogs/daily_quests.py index 709edc1..83c8691 100644 --- a/cogs/daily_quests.py +++ b/cogs/daily_quests.py @@ -30,7 +30,7 @@ async def daily_quest(self, ctx: commands.Context): try: # Get quest progress - quest_date, quizzes, voted, completed, freezes, _ = await get_daily_quest_progress(user_id) + quest_date, quizzes, counting_numbers, quiz_done, counting_done, freeze_units, save_units = await get_daily_quest_progress(user_id) # Create embed embed = discord.Embed( @@ -40,27 +40,34 @@ async def daily_quest(self, ctx: commands.Context): ) # Quest tasks - quiz_status = "Done" if quizzes >= 5 else f"{quizzes}/5" - + quiz_status = "Done" if quiz_done == 1 else f"{quizzes}/5" + count_status = "Done" if counting_done == 1 else f"{counting_numbers}/5" + tasks = f""" - **Solve 5 Basic Quizzes** {quiz_status} - Answer <#1398986762352857129> quiz questions correctly to complete the quest! - - *Note: Top.gg voting will be added soon for bonus rewards!* + **Answer 5 Quiz Questions** {quiz_status} + Answer in the quiz channel to progress. + + **Count 5 Numbers** {count_status} + Count in your server's counting channel to progress. """ embed.add_field(name="Quest Tasks", value=tasks, inline=False) # Rewards section - if completed == 1: - reward_text = "**Quest Completed!** You earned:\n• 1 Streak Freeze" - else: - reward_text = "Complete all tasks to earn:\n• 1 Streak Freeze (protects your streak)" + reward_text = ( + "Each quest completion gives:\n" + "• **0.2** Streak Freeze\n" + "• **0.5** Save\n\n" + "Max inventory: **2.0** Streak Freezes, **4.0** Saves" + ) embed.add_field(name="Rewards", value=reward_text, inline=False) # Current inventory - inventory = f"Streak Freezes: **{freezes}**" + inventory = ( + f"Streak Freezes: **{freeze_units/10:.1f}/2.0**\n" + f"Saves: **{save_units/10:.1f}/4.0**" + ) embed.add_field(name="Your Inventory", value=inventory, inline=False) # Footer @@ -79,7 +86,7 @@ async def inventory(self, ctx: commands.Context): user_id = ctx.author.id try: - freezes, _ = await get_quest_rewards(user_id) + freeze_units, save_units = await get_quest_rewards(user_id) embed = discord.Embed( title="Your Inventory", @@ -90,10 +97,18 @@ async def inventory(self, ctx: commands.Context): # Streak Freezes freeze_desc = "Protect your quiz streak when you answer incorrectly.\nAutomatically used when needed." embed.add_field( - name=f"Streak Freezes: {freezes}", + name=f"Streak Freezes: {freeze_units/10:.1f}/2.0", value=freeze_desc, inline=False ) + + # Saves + save_desc = "Protect the counting game if you ruin the count.\nUsed automatically when you mess up." + embed.add_field( + name=f"Saves: {save_units/10:.1f}/4.0", + value=save_desc, + inline=False, + ) # How to earn more embed.add_field( @@ -118,7 +133,7 @@ async def daily_quest_slash(self, interaction: discord.Interaction): try: # Get quest progress - quest_date, quizzes, voted, completed, freezes, _ = await get_daily_quest_progress(user_id) + quest_date, quizzes, counting_numbers, quiz_done, counting_done, freeze_units, save_units = await get_daily_quest_progress(user_id) # Create embed embed = discord.Embed( @@ -128,27 +143,34 @@ async def daily_quest_slash(self, interaction: discord.Interaction): ) # Quest tasks - quiz_status = "Done" if quizzes >= 5 else f"{quizzes}/5" - + quiz_status = "Done" if quiz_done == 1 else f"{quizzes}/5" + count_status = "Done" if counting_done == 1 else f"{counting_numbers}/5" + tasks = f""" - **Solve 5 Basic Quizzes** {quiz_status} - *Answer CodeBuddy quiz questions correctly to complete the quest!* - - *Note: Top.gg voting will be added soon for bonus rewards!* + **Answer 5 Quiz Questions** {quiz_status} + *Answer CodeBuddy quiz questions correctly to progress.* + + **Count 5 Numbers** {count_status} + *Count in your server's counting channel to progress.* """ embed.add_field(name="Quest Tasks", value=tasks, inline=False) # Rewards section - if completed == 1: - reward_text = "**Quest Completed!** You earned:\n• 1 Streak Freeze" - else: - reward_text = "Complete all tasks to earn:\n• 1 Streak Freeze (protects your streak)" + reward_text = ( + "Each quest completion gives:\n" + "• **0.2** Streak Freeze\n" + "• **0.5** Save\n\n" + "Max inventory: **2.0** Streak Freezes, **4.0** Saves" + ) embed.add_field(name="Rewards", value=reward_text, inline=False) # Current inventory - inventory = f"Streak Freezes: **{freezes}**" + inventory = ( + f"Streak Freezes: **{freeze_units/10:.1f}/2.0**\n" + f"Saves: **{save_units/10:.1f}/4.0**" + ) embed.add_field(name="Your Inventory", value=inventory, inline=False) # Footer diff --git a/utils/codebuddy_database.py b/utils/codebuddy_database.py index e24c497..0f8864f 100644 --- a/utils/codebuddy_database.py +++ b/utils/codebuddy_database.py @@ -33,11 +33,54 @@ async def init_db(): ) """) - # Check for missing columns in daily_quests + # Check for missing columns in daily_quests (lightweight migrations) cursor = await db.execute("PRAGMA table_info(daily_quests)") dq_columns = [row[1] async for row in cursor] + + # Very old DBs may miss the legacy `saves` column. if "saves" not in dq_columns: await db.execute("ALTER TABLE daily_quests ADD COLUMN saves REAL NOT NULL DEFAULT 0") + dq_columns.append("saves") + + # Inventory balances stored as integer tenths to avoid float drift. + # 10 units = 1.0 item. Rewards: +0.2 freeze = +2 units, +0.5 save = +5 units. + if "streak_freeze_units" not in dq_columns: + await db.execute( + "ALTER TABLE daily_quests ADD COLUMN streak_freeze_units INTEGER NOT NULL DEFAULT 0" + ) + # Migrate existing integer streak_freezes -> units (best effort) + await db.execute( + "UPDATE daily_quests SET streak_freeze_units = COALESCE(streak_freezes, 0) * 10" + ) + + if "save_units" not in dq_columns: + await db.execute( + "ALTER TABLE daily_quests ADD COLUMN save_units INTEGER NOT NULL DEFAULT 0" + ) + # Migrate existing saves (REAL) -> units (best effort) + await db.execute( + "UPDATE daily_quests SET save_units = CAST(ROUND(COALESCE(saves, 0) * 10) AS INTEGER)" + ) + + # New daily quest: count 5 numbers in the counting channel. + if "counting_numbers" not in dq_columns: + await db.execute( + "ALTER TABLE daily_quests ADD COLUMN counting_numbers INTEGER NOT NULL DEFAULT 0" + ) + + # Track individual completion of each quest so rewards are granted per-quest. + if "quiz_quest_completed" not in dq_columns: + await db.execute( + "ALTER TABLE daily_quests ADD COLUMN quiz_quest_completed INTEGER NOT NULL DEFAULT 0" + ) + + if "counting_quest_completed" not in dq_columns: + await db.execute( + "ALTER TABLE daily_quests ADD COLUMN counting_quest_completed INTEGER NOT NULL DEFAULT 0" + ) + + # Keep the old columns around for backward compatibility (streak_freezes/saves), + # but new code reads/writes *_units. # Weekly leaderboard table # Note: user_id is NOT a primary key here because we might want to store history, @@ -109,6 +152,15 @@ async def init_db(): ) """) + # Guild save pool for counting mistakes. + # Stored as tenths (10 units = 1.0 save). + await db.execute(""" + CREATE TABLE IF NOT EXISTS counting_guild_saves ( + guild_id INTEGER PRIMARY KEY, + save_units INTEGER NOT NULL DEFAULT 0 + ) + """) + # Truth or Dare table await db.execute(""" CREATE TABLE IF NOT EXISTS tod_questions ( @@ -128,6 +180,80 @@ async def init_db(): await db.commit() await migrate_leaderboard() # Prüft und fügt fehlende Spalten hinzu + +MAX_STREAK_FREEZE_UNITS = 20 # 2.0 +MAX_SAVE_UNITS = 40 # 4.0 +USE_ITEM_UNITS = 10 # 1.0 +QUEST_REWARD_FREEZE_UNITS = 2 # 0.2 +QUEST_REWARD_SAVE_UNITS = 5 # 0.5 + + +def _coerce_date(value: object) -> datetime.date: + if isinstance(value, datetime.date): + return value + if isinstance(value, str): + try: + return datetime.datetime.strptime(value, "%Y-%m-%d").date() + except Exception: + pass + return datetime.date.today() + + +def _clamp_int(value: int, min_value: int, max_value: int) -> int: + return max(min_value, min(max_value, int(value))) + + +def _format_units(units: int) -> str: + # Display with at most 1 decimal (units are tenths). + whole, tenth = divmod(int(units), 10) + if tenth == 0: + return str(whole) + return f"{whole}.{tenth}" + + +async def _ensure_daily_quest_row(db: aiosqlite.Connection, user_id: int) -> None: + today = datetime.date.today() + + # Ensure row exists + await db.execute( + """ + INSERT OR IGNORE INTO daily_quests ( + user_id, quest_date, + quizzes_completed, counting_numbers, + quiz_quest_completed, counting_quest_completed, + voted_today, quest_completed, + streak_freezes, bonus_hints, saves, + streak_freeze_units, save_units + ) + VALUES (?, ?, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + """, + (user_id, today), + ) + + # Reset daily progress if date has rolled over + cursor = await db.execute( + "SELECT quest_date FROM daily_quests WHERE user_id = ?", + (user_id,), + ) + row = await cursor.fetchone() + quest_date = _coerce_date(row[0]) if row else today + if quest_date < today: + await db.execute( + """ + UPDATE daily_quests + SET quest_date = ?, + quizzes_completed = 0, + counting_numbers = 0, + quiz_quest_completed = 0, + counting_quest_completed = 0, + voted_today = 0, + quest_completed = 0 + WHERE user_id = ? + """, + (today, user_id), + ) + + async def populate_tod_questions(db): """Populate the TOD table with default questions.""" truths = [ @@ -406,125 +532,190 @@ async def get_score_gap(user_id: int): async def get_daily_quest_progress(user_id: int): """ Get the daily quest progress for a user. - Returns: (quest_date, quizzes_completed, voted_today, quest_completed, streak_freezes, bonus_hints) + Returns: (quest_date, quizzes_completed, counting_numbers, quiz_completed, counting_completed, streak_freeze_units, save_units) """ today = datetime.date.today() - + async with aiosqlite.connect(DB_PATH) as db: + await _ensure_daily_quest_row(db, user_id) + cursor = await db.execute( - "SELECT quest_date, quizzes_completed, voted_today, quest_completed, streak_freezes, bonus_hints FROM daily_quests WHERE user_id = ?", - (user_id,) + """ + SELECT quest_date, + quizzes_completed, + counting_numbers, + quiz_quest_completed, + counting_quest_completed, + streak_freeze_units, + save_units + FROM daily_quests + WHERE user_id = ? + """, + (user_id,), ) row = await cursor.fetchone() - + if not row: - # Initialize new quest entry for today - await db.execute( - "INSERT INTO daily_quests (user_id, quest_date, quizzes_completed, voted_today, quest_completed, streak_freezes, bonus_hints) VALUES (?, ?, 0, 0, 0, 0, 0)", - (user_id, today) - ) - await db.commit() - return (today, 0, 0, 0, 0, 0) - - quest_date_str, quizzes, voted, completed, freezes, hints = row - quest_date = datetime.datetime.strptime(quest_date_str, "%Y-%m-%d").date() - - # Check if quest is from a previous day - reset if so - if quest_date < today: - await db.execute( - "UPDATE daily_quests SET quest_date = ?, quizzes_completed = 0, voted_today = 0, quest_completed = 0 WHERE user_id = ?", - (today, user_id) - ) - await db.commit() - return (today, 0, 0, 0, freezes, hints) - - return (quest_date, quizzes, voted, completed, freezes, hints) + return (today, 0, 0, 0, 0, 0, 0) + + quest_date = _coerce_date(row[0]) + quizzes = int(row[1] or 0) + counting_numbers = int(row[2] or 0) + quiz_done = int(row[3] or 0) + counting_done = int(row[4] or 0) + freeze_units = int(row[5] or 0) + save_units = int(row[6] or 0) + + return (quest_date, quizzes, counting_numbers, quiz_done, counting_done, freeze_units, save_units) async def increment_quest_quiz_count(user_id: int): """ - Increment the quiz count for today's quest. - Returns True if quest was completed with this quiz. + Increment the quiz task progress for today's quest. + Returns True if the *quiz quest* was completed with this answer. """ async with aiosqlite.connect(DB_PATH) as db: - # Get current progress - progress = await get_daily_quest_progress(user_id) - _, quizzes, voted, completed, freezes, hints = progress - - # Don't increment if already at 5 or more + await _ensure_daily_quest_row(db, user_id) + + cursor = await db.execute( + """ + SELECT quizzes_completed, quiz_quest_completed, streak_freeze_units, save_units + FROM daily_quests + WHERE user_id = ? + """, + (user_id,), + ) + row = await cursor.fetchone() + quizzes = int(row[0] or 0) if row else 0 + quest_done = int(row[1] or 0) if row else 0 + freeze_units = int(row[2] or 0) if row else 0 + save_units = int(row[3] or 0) if row else 0 + + if quest_done == 1: + return False + if quizzes >= 5: return False - - new_count = quizzes + 1 - - # Check if quest is now complete (only requires 5 quizzes) - quest_complete = (new_count >= 5 and completed == 0) - + + new_quizzes = min(5, quizzes + 1) + quest_complete = new_quizzes >= 5 + if quest_complete: - # Quest completed! Award rewards + new_freeze_units = _clamp_int(freeze_units + QUEST_REWARD_FREEZE_UNITS, 0, MAX_STREAK_FREEZE_UNITS) + new_save_units = _clamp_int(save_units + QUEST_REWARD_SAVE_UNITS, 0, MAX_SAVE_UNITS) await db.execute( - "UPDATE daily_quests SET quizzes_completed = ?, quest_completed = 1, streak_freezes = streak_freezes + 1, bonus_hints = bonus_hints + 1 WHERE user_id = ?", - (new_count, user_id) + """ + UPDATE daily_quests + SET quizzes_completed = ?, + quiz_quest_completed = 1, + streak_freeze_units = ?, + save_units = ? + WHERE user_id = ? + """, + (new_quizzes, new_freeze_units, new_save_units, user_id), ) else: await db.execute( "UPDATE daily_quests SET quizzes_completed = ? WHERE user_id = ?", - (new_count, user_id) + (new_quizzes, user_id), ) - + await db.commit() return quest_complete -async def mark_quest_voted(user_id: int): - """ - Mark that the user has voted today. - Returns True if quest was completed with this vote. + +async def increment_quest_counting_count(user_id: int): + """Increment the counting task progress for today's quest. + + Returns True if the *counting quest* was completed with this count. """ async with aiosqlite.connect(DB_PATH) as db: - # Get current progress - progress = await get_daily_quest_progress(user_id) - _, quizzes, voted, completed, freezes, hints = progress - - # Don't mark if already voted - if voted == 1: + await _ensure_daily_quest_row(db, user_id) + + cursor = await db.execute( + """ + SELECT counting_numbers, counting_quest_completed, streak_freeze_units, save_units + FROM daily_quests + WHERE user_id = ? + """, + (user_id,), + ) + row = await cursor.fetchone() + counted = int(row[0] or 0) if row else 0 + quest_done = int(row[1] or 0) if row else 0 + freeze_units = int(row[2] or 0) if row else 0 + save_units = int(row[3] or 0) if row else 0 + + if quest_done == 1: return False - - # Check if quest is now complete - quest_complete = (quizzes >= 5 and completed == 0) - + + if counted >= 5: + return False + + new_counted = min(5, counted + 1) + quest_complete = new_counted >= 5 + if quest_complete: - # Quest completed! Award rewards + new_freeze_units = _clamp_int(freeze_units + QUEST_REWARD_FREEZE_UNITS, 0, MAX_STREAK_FREEZE_UNITS) + new_save_units = _clamp_int(save_units + QUEST_REWARD_SAVE_UNITS, 0, MAX_SAVE_UNITS) await db.execute( - "UPDATE daily_quests SET voted_today = 1, quest_completed = 1, streak_freezes = streak_freezes + 1, bonus_hints = bonus_hints + 1 WHERE user_id = ?", - (user_id,) + """ + UPDATE daily_quests + SET counting_numbers = ?, + counting_quest_completed = 1, + streak_freeze_units = ?, + save_units = ? + WHERE user_id = ? + """, + (new_counted, new_freeze_units, new_save_units, user_id), ) else: await db.execute( - "UPDATE daily_quests SET voted_today = 1 WHERE user_id = ?", - (user_id,) + "UPDATE daily_quests SET counting_numbers = ? WHERE user_id = ?", + (new_counted, user_id), ) - + await db.commit() return quest_complete +async def mark_quest_voted(user_id: int): + """ + Mark that the user has voted today. + Legacy helper (voting quest not currently used). + + Returns False. + """ + async with aiosqlite.connect(DB_PATH) as db: + await _ensure_daily_quest_row(db, user_id) + + # Keep a flag for future use; do not award items from voting. + await db.execute( + "UPDATE daily_quests SET voted_today = 1 WHERE user_id = ?", + (user_id,), + ) + await db.commit() + return False + async def use_streak_freeze(user_id: int): """ Use a streak freeze to prevent streak reset. Returns True if freeze was available and used. """ async with aiosqlite.connect(DB_PATH) as db: + await _ensure_daily_quest_row(db, user_id) + cursor = await db.execute( - "SELECT streak_freezes FROM daily_quests WHERE user_id = ?", - (user_id,) + "SELECT streak_freeze_units FROM daily_quests WHERE user_id = ?", + (user_id,), ) row = await cursor.fetchone() - - if not row or row[0] <= 0: + + current_units = int(row[0] or 0) if row else 0 + if current_units < USE_ITEM_UNITS: return False - - # Use one freeze + await db.execute( - "UPDATE daily_quests SET streak_freezes = streak_freezes - 1 WHERE user_id = ?", - (user_id,) + "UPDATE daily_quests SET streak_freeze_units = streak_freeze_units - ? WHERE user_id = ?", + (USE_ITEM_UNITS, user_id), ) await db.commit() return True @@ -554,18 +745,85 @@ async def use_bonus_hint(user_id: int): async def get_quest_rewards(user_id: int): """ - Get the current number of streak freezes and bonus hints. - Returns: (streak_freezes, bonus_hints) + Get the current inventory balances. + Returns: (streak_freeze_units, save_units) """ async with aiosqlite.connect(DB_PATH) as db: + await _ensure_daily_quest_row(db, user_id) cursor = await db.execute( - "SELECT streak_freezes, bonus_hints FROM daily_quests WHERE user_id = ?", - (user_id,) + "SELECT streak_freeze_units, save_units FROM daily_quests WHERE user_id = ?", + (user_id,), ) row = await cursor.fetchone() - if not row: return (0, 0) - - return row + return (int(row[0] or 0), int(row[1] or 0)) + + +async def get_user_save_units(user_id: int) -> int: + async with aiosqlite.connect(DB_PATH) as db: + await _ensure_daily_quest_row(db, user_id) + cursor = await db.execute("SELECT save_units FROM daily_quests WHERE user_id = ?", (user_id,)) + row = await cursor.fetchone() + return int(row[0] or 0) if row else 0 + + +async def try_use_user_save(user_id: int) -> bool: + """Consume 1.0 personal save if available.""" + async with aiosqlite.connect(DB_PATH) as db: + await _ensure_daily_quest_row(db, user_id) + cursor = await db.execute("SELECT save_units FROM daily_quests WHERE user_id = ?", (user_id,)) + row = await cursor.fetchone() + units = int(row[0] or 0) if row else 0 + if units < USE_ITEM_UNITS: + return False + await db.execute( + "UPDATE daily_quests SET save_units = save_units - ? WHERE user_id = ?", + (USE_ITEM_UNITS, user_id), + ) + await db.commit() + return True + + +async def get_guild_save_units(guild_id: int) -> int: + async with aiosqlite.connect(DB_PATH) as db: + cursor = await db.execute( + "SELECT save_units FROM counting_guild_saves WHERE guild_id = ?", + (guild_id,), + ) + row = await cursor.fetchone() + return int(row[0] or 0) if row else 0 + + +async def add_guild_save_units(guild_id: int, units: int) -> int: + async with aiosqlite.connect(DB_PATH) as db: + await db.execute( + """ + INSERT INTO counting_guild_saves (guild_id, save_units) + VALUES (?, ?) + ON CONFLICT(guild_id) DO UPDATE SET save_units = save_units + excluded.save_units + """, + (guild_id, int(units)), + ) + await db.commit() + return await get_guild_save_units(guild_id) + + +async def try_use_guild_save(guild_id: int) -> bool: + """Consume 1.0 guild save if available.""" + async with aiosqlite.connect(DB_PATH) as db: + cursor = await db.execute( + "SELECT save_units FROM counting_guild_saves WHERE guild_id = ?", + (guild_id,), + ) + row = await cursor.fetchone() + units = int(row[0] or 0) if row else 0 + if units < USE_ITEM_UNITS: + return False + await db.execute( + "UPDATE counting_guild_saves SET save_units = save_units - ? WHERE guild_id = ?", + (USE_ITEM_UNITS, guild_id), + ) + await db.commit() + return True From 1ce32116d8a2d8bab455bca31d357aed0f8e07e1 Mon Sep 17 00:00:00 2001 From: youngcoder45 Date: Sun, 5 Apr 2026 16:50:55 +0530 Subject: [PATCH 3/3] Add a prefix command to view saves in guild rn --- README.md | 22 ++++++++++++---------- cogs/counting.py | 13 +++++++++++++ cogs/help.py | 4 ++-- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 6eb0308..53d429a 100644 --- a/README.md +++ b/README.md @@ -95,24 +95,23 @@ Complete daily challenges to earn powerful rewards! Inspired by popular quest sy **Features:** - **Daily Checklist**: Reset every 24 hours with fresh challenges - **Quest Tasks**: - - Solve 5 Basic CodeBuddy Quizzes - - Vote for the Bot on top.gg (coming soon!) + - Answer 5 CodeBuddy quiz questions + - Count 5 valid numbers in the counting channel - **Rewards**: - - **Streak Freezes**: Automatically protect your quiz streak when you answer wrong - - **Bonus Hints**: Use hints to eliminate wrong answers (ephemeral messages) + - **Streak Freezes**: Protect your quiz streak when you answer wrong (consumes 1.0) + - **Saves**: Protect the counting game when you ruin the count (consumes 1.0) - **Progress Tracking**: Monitor your daily quest completion in real-time **Commands:** - `?dailyquest` / `?dq` / `?quests` - View daily quest progress - `/dailyquest` - View quest progress (slash command) -- `?bonushint` / `?hint` - Use a bonus hint on active quiz -- `?inventory` / `?inv` - Check your streak freezes and bonus hints +- `?inventory` / `?inv` - Check your streak freezes and saves **How It Works:** -1. Complete 5 quiz questions correctly -2. Vote for the bot (when available) -3. Earn 1 Streak Freeze + 1 Bonus Hint -4. Use rewards strategically to maintain your streak and climb leaderboards! +1. Complete either quest (Quiz or Counting) +2. Each quest completion awards **0.2** streak-freeze + **0.5** save +3. Inventory caps: max **2.0** streak-freezes and **4.0** saves +4. Use rewards strategically to maintain your streak and protect counting! ### ** Fun Commands** Entertainment and engagement features: @@ -165,9 +164,12 @@ Celebrate community birthdays: Run a server counting game with anti-grief protections and highscores: - **Set Channel**: `/setcountingchannel ` - Admin-only, choose the counting channel - **Double-count Warnings**: Counting twice in a row gives `⚠️` warnings (3 warnings triggers a fail) +- **Save Protection**: If someone posts the wrong number, the bot tries to consume a **personal save** first, then a **server save**; otherwise the count resets to 0 - **Deleted Number Logging**: If a valid counting number is deleted, the bot announces who deleted it - **Highscore Marker**: When the server reaches/ties the highscore, the message is marked with ✅ + 🏆 until the count is ruined - **Highscore Table**: `?highscoretable` / `/highscoretable` (and `?highscores`) - View recent highscore history +- **Donate Saves to Server Pool**: `?donateguild` / `?dg` - Donate **1.0** personal save; server gains **0.5** save +- **View Server Save Pool**: `?guildsaves` - Show current server saves (needs **1.0** to protect a ruined count) ### ** Staff Applications** Collect staff applications via DMs and review them in a configurable channel: diff --git a/cogs/counting.py b/cogs/counting.py index 3426971..feb0778 100644 --- a/cogs/counting.py +++ b/cogs/counting.py @@ -648,6 +648,19 @@ async def donate_guild(self, ctx: commands.Context): f"Your saves: **{new_user_units/10:.1f}** • Server saves: **{new_guild_units/10:.1f}**" ) + + @commands.command(name="guildsaves", aliases=["gsaves", "serversaves", "ssaves"]) + async def guild_saves(self, ctx: commands.Context): + """Show the server save pool used to protect counting mistakes.""" + if not ctx.guild: + return await ctx.send("Server only command.") + + units = await get_guild_save_units(ctx.guild.id) + await ctx.send( + f"Server saves: **{units/10:.1f}**\n" + "(Needs **1.0** server save to protect a ruined count.)" + ) + @commands.hybrid_command(name="highscoretable", aliases=["highscores"], help="Show recent counting highscores") async def highscore_table(self, ctx: commands.Context): if not ctx.guild: diff --git a/cogs/help.py b/cogs/help.py index d274c15..b06284f 100644 --- a/cogs/help.py +++ b/cogs/help.py @@ -42,8 +42,8 @@ "codebuddyleaderboardcog": "View coding leaderboards, weekly stats, and streaks", "codebuddyquizcog": "Test your coding knowledge with quizzes", "codebuddyhelpcog": "Help and information for CodeBuddy features", - "dailyquestscog": "Complete daily challenges to earn rewards! Solve quizzes, vote, and earn streak freezes & bonus hints", - "counting": "Counting game with highscores, warnings, and leaderboards", + "dailyquestscog": "Complete daily quests to earn partial streak freezes and saves (quiz + counting tasks)", + "counting": "Counting game with warnings, highscores, and save protection", "staffapplications": "Staff application panel, review buttons, and admin config", "suggestions": "Submit suggestions with voting reactions + discussion threads", "bumpleaderboard": "Track Disboard /bump activity with leaderboards and stats",