510 lines
20 KiB
Python
Executable File
510 lines
20 KiB
Python
Executable File
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))
|