import discord from discord.ext import commands, tasks from utils.sql_commands import DatabaseManager from utils.bank_functions import bank_data, update_money from random import choice from datetime import datetime, timedelta import logging TICKET_TYPES = { "standard": {"price": 500, "weight": 1}, "premium": {"price": 2000, "weight": 5}, } LOTTERY_INTERVAL_HOURS = 24 # Draw every 24 hours MAX_TICKETS_PER_USER = 10 ROLLOVER_BONUS = 500 class Lottery(commands.Cog): def __init__(self, client): self.client = client self.db = DatabaseManager() self.lottery_draw.start() def cog_unload(self): self.lottery_draw.cancel() def get_jackpot(self) -> int: try: result = self.db.fetch_one("SELECT jackpot FROM lottery_state WHERE id = 1") return int(result["jackpot"]) if result else 0 except Exception as e: logging.error(f"Error fetching jackpot: {e}") return 0 def set_jackpot(self, amount: int) -> None: try: self.db.execute_query( "UPDATE lottery_state SET jackpot = %s WHERE id = 1", (amount,) ) except Exception as e: logging.error(f"Error setting jackpot: {e}") def add_to_jackpot(self, amount: int) -> None: try: self.db.execute_query( "UPDATE lottery_state SET jackpot = jackpot + %s WHERE id = 1", (amount,), ) except Exception as e: logging.error(f"Error adding to jackpot: {e}") def get_last_draw_time(self) -> datetime: try: result = self.db.fetch_one( "SELECT last_draw FROM lottery_draw_time WHERE id = 1" ) last_draw = result["last_draw"] if result and result["last_draw"] else None if last_draw is None: # Set to now if missing and update DB now = datetime.utcnow() self.set_last_draw_time(now) return now if isinstance(last_draw, str): try: last_draw = datetime.fromisoformat(last_draw) except Exception: last_draw = datetime.strptime(last_draw, "%Y-%m-%d %H:%M:%S") return last_draw except Exception as e: logging.error(f"Error fetching last draw time: {e}") now = datetime.utcnow() self.set_last_draw_time(now) return now def set_last_draw_time(self, draw_time: datetime) -> None: try: self.db.execute_query( "UPDATE lottery_draw_time SET last_draw = %s WHERE id = 1", (draw_time,) ) except Exception as e: logging.error(f"Error setting last draw time: {e}") def notify_lottery_result(self, user_id: int): try: result = self.db.fetch_one( "SELECT * FROM lottery_results WHERE WINNER_ID = %s AND CLAIMED = 0 ORDER BY DRAW_TIME DESC LIMIT 1", (user_id,), ) if not result: return None # Remove the result after notifying self.db.execute_query( "DELETE FROM lottery_results WHERE ID = %s", (result["ID"],) ) return (True, result["AMOUNT"]) except Exception as e: logging.error(f"Error notifying lottery result: {e}") return None @commands.command( name="buyticket", help=f"Buy one or more lottery tickets for yourself or a group.", ) async def buy_ticket( self, ctx: commands.Context, ticket_type: str = "standard", amount: int = 1, group_id: str | None = None, member: discord.Member | None = None, ): ticket_type = ticket_type.lower() if ticket_type not in TICKET_TYPES: await ctx.reply( f"Invalid ticket type. Choose from: {', '.join(TICKET_TYPES)}" ) return if amount < 1 or amount > MAX_TICKETS_PER_USER: await ctx.reply( f"You can only buy between 1 and {MAX_TICKETS_PER_USER} tickets at once." ) return user = member or ctx.author price = TICKET_TYPES[ticket_type]["price"] * amount try: # Notify about last draw if relevant (only if user is the winner) notify = self.notify_lottery_result(user.id) if notify: await ctx.reply( f"šŸŽ‰ You won the last lottery! Jackpot: {notify[1]:,} coins.", mention_author=False, ) user_data = await bank_data(user) wallet = int(user_data.get("WALLET", 0)) if wallet < price: await ctx.reply( f"You need at least {price} coins to buy {amount} {ticket_type} ticket(s).", mention_author=False, ) return if group_id: group_exists = self.db.fetch_one( "SELECT 1 FROM lottery_groups WHERE group_id = %s", (group_id,) ) in_group = self.db.fetch_one( "SELECT 1 FROM lottery_group_members WHERE group_id = %s AND user_id = %s", (group_id, user.id), ) if not group_exists or not in_group: await ctx.reply( "You must be a member of the group to buy tickets for it." ) return user_ticket_count = self.db.fetch_one( "SELECT COUNT(*) as count FROM lottery_tickets WHERE USERID = %s", (user.id,), )["count"] if user_ticket_count + amount > MAX_TICKETS_PER_USER: await ctx.reply( f"You can only have {MAX_TICKETS_PER_USER} tickets per draw. You currently have {user_ticket_count}." ) return # Deduct ticket price await update_money(user, wallet=-price) self.add_to_jackpot(price) # Store tickets in DB (one row per ticket for higher odds with multiple tickets) for _ in range(amount): self.db.execute_query( "INSERT INTO lottery_tickets (USERID, TIMESTAMP, TICKET_TYPE, group_id) VALUES (%s, %s, %s, %s)", (user.id, datetime.utcnow(), ticket_type, group_id), ) jackpot = self.get_jackpot() await ctx.reply( f"šŸŽŸļø {amount} {ticket_type.capitalize()} ticket(s) bought! Jackpot is now {jackpot:,} coins.", mention_author=False, ) except Exception as e: logging.error(f"Error in buy_ticket: {e}") await ctx.reply( "An error occurred while buying your ticket(s).", mention_author=False ) @commands.command(name="lotterystats", help="Show current lottery stats.") async def lottery_stats(self, ctx: commands.Context): try: # Show last winner and their prize last_result = self.db.fetch_one( "SELECT * FROM lottery_results ORDER BY DRAW_TIME DESC LIMIT 1" ) winner_text = "No draws yet." if last_result: winner = self.client.get_user( last_result["WINNER_ID"] ) or await self.client.fetch_user(last_result["WINNER_ID"]) winner_text = f"Last winner: {winner.mention if winner else 'Unknown'} ({last_result['AMOUNT']:,} coins)" ticket_count = self.db.fetch_one( "SELECT COUNT(*) as count FROM lottery_tickets" )["count"] jackpot = self.get_jackpot() # Check if the user has an unclaimed win unclaimed = self.db.fetch_one( "SELECT * FROM lottery_results WHERE WINNER_ID = %s AND CLAIMED = 0 ORDER BY DRAW_TIME DESC LIMIT 1", (ctx.author.id,), ) winner_note = "" if unclaimed: # Mark as claimed and pay out self.db.execute_query( "UPDATE lottery_results SET CLAIMED = 1 WHERE ID = %s", (unclaimed["ID"],), ) await update_money(ctx.author, wallet=unclaimed["AMOUNT"]) winner_note = f"\nšŸŽ‰ **You have won {unclaimed['AMOUNT']:,} coins! Your prize has been paid out.**" await ctx.reply( f"{winner_text}\n" f"šŸŽ° There are currently **{ticket_count}** tickets in the pool.\n" f"šŸ’° Jackpot: **{jackpot:,}** coins.\n" f"Next draw in: {self.time_until_draw()}" f"{winner_note}", mention_author=False, ) except Exception as e: logging.error(f"Error in lottery_stats: {e}") await ctx.reply( "An error occurred while fetching lottery stats.", mention_author=False ) def time_until_draw(self) -> str: now = datetime.utcnow() last_draw = self.get_last_draw_time() next_draw = last_draw + timedelta(hours=LOTTERY_INTERVAL_HOURS) remaining = next_draw - now if remaining.total_seconds() < 0: return "Drawing soon!" hours, remainder = divmod(int(remaining.total_seconds()), 3600) minutes, seconds = divmod(remainder, 60) return f"{hours}h {minutes}m {seconds}s" @tasks.loop(minutes=1) async def lottery_draw(self): now = datetime.utcnow() last_draw = self.get_last_draw_time() if (now - last_draw).total_seconds() < LOTTERY_INTERVAL_HOURS * 3600: return # Not time yet await self._run_lottery_draw() @commands.command(name="testdraw", help="Manually trigger the lottery draw.") @commands.is_owner() async def testdraw(self, ctx: commands.Context): await ctx.send("Starting lottery draw...") await self._run_lottery_draw() async def _run_lottery_draw(self): try: await self.client.wait_until_ready() tickets = self.db.fetch_all( "SELECT USERID, TICKET_TYPE FROM lottery_tickets" ) jackpot = self.get_jackpot() if not tickets or jackpot <= 0: self.set_jackpot(self.get_jackpot() + ROLLOVER_BONUS) self.set_last_draw_time(datetime.utcnow()) return # Build weighted ticket list weighted_tickets = [] for t in tickets: luck_row = self.db.fetch_one( "SELECT LUCK FROM lottery_luck WHERE USERID = %s", (t["USERID"],) ) luck = luck_row["LUCK"] if luck_row else 0 ticket_type_weight = TICKET_TYPES[t["TICKET_TYPE"]]["weight"] weight = ticket_type_weight * (1 + luck) weighted_tickets.extend([t["USERID"]] * weight) winner_id = choice(weighted_tickets) # Find group for winning ticket BEFORE deleting tickets group_id = self.db.fetch_one( "SELECT group_id FROM lottery_tickets WHERE USERID = %s LIMIT 1", (winner_id,), )["group_id"] self.db.execute_query("DELETE FROM lottery_tickets") # Reset for next round if group_id: members = self.db.fetch_all( "SELECT user_id FROM lottery_group_members WHERE group_id = %s", (group_id,), ) member_ids = [m["user_id"] for m in members] split_prize = (jackpot // 2) // len(member_ids) for uid in member_ids: member = self.client.get_user(uid) or await self.client.fetch_user( uid ) await update_money(member, wallet=split_prize) # Store group win in results with WIN_TYPE='group' self.db.execute_query( "INSERT INTO lottery_results (WINNER_ID, AMOUNT, DRAW_TIME, CLAIMED, WIN_TYPE) VALUES (%s, %s, %s, 0, %s)", (group_id, jackpot // 2, datetime.utcnow(), "group"), ) else: # Store solo win in results with WIN_TYPE='user' self.db.execute_query( "INSERT INTO lottery_results (WINNER_ID, AMOUNT, DRAW_TIME, CLAIMED, WIN_TYPE) VALUES (%s, %s, %s, 0, %s)", (winner_id, jackpot // 2, datetime.utcnow(), "user"), ) # Carry over half the jackpot (rounded down) carryover = jackpot // 2 self.set_jackpot(carryover) self.set_last_draw_time(datetime.utcnow()) # Reset winner's luck, increment others' self.db.execute_query( "UPDATE lottery_luck SET LUCK = 0 WHERE USERID = %s", (winner_id,) ) self.db.execute_query( "UPDATE lottery_luck SET LUCK = LUCK + 1 WHERE USERID != %s", (winner_id,), ) except Exception as e: logging.error(f"Error in _run_lottery_draw: {e}") @lottery_draw.before_loop async def before_lottery_draw(self): await self.client.wait_until_ready() @commands.command(name="lotteryhistory") async def lottery_history(self, ctx): results = self.db.fetch_all( "SELECT * FROM lottery_results ORDER BY DRAW_TIME DESC LIMIT 10" ) if not results: await ctx.reply("No lottery draws yet.") return lines = [] for res in results: if res.get("WIN_TYPE") == "group": lines.append( f"{res['DRAW_TIME'].strftime('%Y-%m-%d')}: Group `{res['WINNER_ID']}` won {res['AMOUNT']:,} coins" ) else: winner = self.client.get_user( res["WINNER_ID"] ) or await self.client.fetch_user(res["WINNER_ID"]) lines.append( f"{res['DRAW_TIME'].strftime('%Y-%m-%d')}: {winner.mention if winner else 'Unknown'} won {res['AMOUNT']:,} coins" ) await ctx.reply("\n".join(lines)) @commands.command(name="lotteryleaderboard") async def lottery_leaderboard(self, ctx): winners = self.db.fetch_all( "SELECT WINNER_ID, COUNT(*) as wins, SUM(AMOUNT) as total FROM lottery_results GROUP BY WINNER_ID ORDER BY wins DESC LIMIT 10" ) if not winners: await ctx.reply("No winners yet.") return lines = [] for w in winners: user = self.client.get_user(w["WINNER_ID"]) or await self.client.fetch_user( w["WINNER_ID"] ) lines.append( f"{user.mention if user else 'Unknown'}: {w['wins']} wins, {w['total']:,} coins" ) await ctx.reply("\n".join(lines)) @commands.command(name="initluck") @commands.is_owner() async def init_luck(self, ctx): """Initialize or reset the luck of a user.""" user = ctx.author self.db.execute_query( "INSERT IGNORE INTO lottery_luck (USERID, LUCK) VALUES (%s, 0)", (user.id,) ) await ctx.reply(f"Your luck has been initialized/reset.") @commands.command(name="creategroup") async def create_group(self, ctx, group_id: str): exists = self.db.fetch_one( "SELECT 1 FROM lottery_groups WHERE group_id = %s", (group_id,) ) if exists: await ctx.reply("A group with that ID already exists.") return self.db.execute_query( "INSERT INTO lottery_groups (group_id, creator_id) VALUES (%s, %s)", (group_id, ctx.author.id), ) self.db.execute_query( "INSERT INTO lottery_group_members (group_id, user_id) VALUES (%s, %s)", (group_id, ctx.author.id), ) await ctx.reply(f"Group `{group_id}` created and you have joined it.") @commands.command(name="joingroup") async def join_group(self, ctx, group_id: str): exists = self.db.fetch_one( "SELECT 1 FROM lottery_groups WHERE group_id = %s", (group_id,) ) if not exists: await ctx.reply("That group does not exist.") return already = self.db.fetch_one( "SELECT 1 FROM lottery_group_members WHERE group_id = %s AND user_id = %s", (group_id, ctx.author.id), ) if already: await ctx.reply("You are already in this group.") return self.db.execute_query( "INSERT INTO lottery_group_members (group_id, user_id) VALUES (%s, %s)", (group_id, ctx.author.id), ) await ctx.reply(f"You have joined group `{group_id}`.") @commands.command(name="leavegroup") async def leave_group(self, ctx, group_id: str): self.db.execute_query( "DELETE FROM lottery_group_members WHERE group_id = %s AND user_id = %s", (group_id, ctx.author.id), ) await ctx.reply(f"You have left group `{group_id}`.") @commands.command(name="deletegroup") async def delete_group(self, ctx, group_id: str): # Check if group exists and if user is creator group = self.db.fetch_one( "SELECT creator_id FROM lottery_groups WHERE group_id = %s", (group_id,) ) if not group: await ctx.reply("That group does not exist.") return if group["creator_id"] != ctx.author.id: await ctx.reply("Only the group creator can delete this group.") return # Check if group is empty (no members except possibly the creator) members = self.db.fetch_all( "SELECT user_id FROM lottery_group_members WHERE group_id = %s", (group_id,) ) if len(members) > 1 or ( len(members) == 1 and members[0]["user_id"] != ctx.author.id ): await ctx.reply( "You can only delete a group if it is empty (no members except you)." ) return # Delete group and membership self.db.execute_query( "DELETE FROM lottery_group_members WHERE group_id = %s", (group_id,) ) self.db.execute_query( "DELETE FROM lottery_groups WHERE group_id = %s", (group_id,) ) await ctx.reply(f"Group `{group_id}` has been deleted.") @commands.command(name="refundtickets") async def refund_tickets(self, ctx, amount: int = 1): user = ctx.author tickets = self.db.fetch_all( "SELECT * FROM lottery_tickets WHERE USERID = %s", (user.id,) ) if not tickets: await ctx.reply("You have no tickets to refund for this draw.") return if amount is None: amount = len(tickets) if amount < 1 or amount > len(tickets): await ctx.reply( f"You can only refund between 1 and {len(tickets)} tickets." ) return # Refund only the specified amount tickets_to_refund = tickets[:amount] total_refund = 0 ticket_ids = [] for ticket in tickets_to_refund: price = TICKET_TYPES[ticket["TICKET_TYPE"]]["price"] refund = int(price * 0.8) total_refund += refund ticket_ids.append(ticket["ID"]) # Remove only the refunded tickets format_strings = ",".join(["%s"] * len(ticket_ids)) self.db.execute_query( f"DELETE FROM lottery_tickets WHERE ID IN ({format_strings})", tuple(ticket_ids), ) await update_money(user, wallet=total_refund) await ctx.reply( f"{amount} ticket(s) refunded for {total_refund:,} coins (80% of purchase price)." ) async def setup(client): await client.add_cog(Lottery(client))