feat(economy): add /lottery - daily draw, weighted winner takes the pot
Buy tickets (200 coins each, max 100/draw); one winner is drawn daily at 21:00 Tallinn time weighted by ticket count and credited the whole pot. Coin-conserving by design: each ticket's cost is deducted at purchase and the winner is minted exactly the sum of all ticket spend - no shared pot record, so no cross-period race. Ticket state lives per-user keyed by draw period (full scan only at draw time and for the pot view). - New lottery.py: TICKET_COST/MAX_TICKETS/DRAW_HOUR, pure period_for, and do_buy_ticket / get_lottery_state / do_lottery_draw. New lottery_tickets + lottery_period schema fields (period added to _TEXT_FIELDS). - /lottery [kogus] command (view or buy) with full failure handling. - Scheduled lottery_draw_daily loop in bot.py (both profiles; each draws its own collection), announcing to the optional LOTTERY_CHANNEL_ID (config + .env). 14 tests: period boundary, buy/accumulate/reset/caps/guards, pot state, draw payout with coin conservation, and the more-tickets-wins-more weighting. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013VbAVsrZuYesea99mPMmPT
This commit is contained in:
@@ -5,6 +5,7 @@ import datetime
|
||||
import random
|
||||
import time
|
||||
from collections.abc import Awaitable, Callable, MutableSet
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import discord
|
||||
from discord import app_commands
|
||||
@@ -15,6 +16,8 @@ import strings as S
|
||||
|
||||
from ._replies import reply_db_error
|
||||
|
||||
_TALLINN = ZoneInfo("Europe/Tallinn")
|
||||
|
||||
|
||||
def register_economy_extra_commands(
|
||||
tree: app_commands.CommandTree,
|
||||
@@ -554,6 +557,64 @@ def register_economy_extra_commands(
|
||||
)
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# /lottery - daily draw, one weighted winner takes the pot
|
||||
# -----------------------------------------------------------------------
|
||||
@tree.command(name="lottery", description=S.CMD["lottery"])
|
||||
@app_commands.describe(kogus=S.OPT["lottery_kogus"])
|
||||
async def cmd_lottery(interaction: discord.Interaction, kogus: int | None = None):
|
||||
period = economy.period_for(datetime.datetime.now(_TALLINN))
|
||||
|
||||
if kogus is None:
|
||||
state = await economy.get_lottery_state(period, interaction.user.id)
|
||||
embed = discord.Embed(
|
||||
title=S.LOTTERY_UI["title"],
|
||||
description=S.LOTTERY_UI["desc"].format(
|
||||
draw_time=f"{economy.DRAW_HOUR}:00", cost=coin(state["ticket_cost"])
|
||||
),
|
||||
color=0xF4C430,
|
||||
)
|
||||
embed.add_field(name=S.LOTTERY_UI["f_pot"], value=coin(state["pot"]), inline=True)
|
||||
embed.add_field(name=S.LOTTERY_UI["f_players"], value=str(state["participants"]), inline=True)
|
||||
if state["your_tickets"]:
|
||||
chance = round(state["your_tickets"] / state["total_tickets"] * 100, 1)
|
||||
embed.add_field(
|
||||
name=S.LOTTERY_UI["f_your"],
|
||||
value=S.LOTTERY_UI["your_val"].format(tickets=state["your_tickets"], chance=chance),
|
||||
inline=False,
|
||||
)
|
||||
else:
|
||||
embed.add_field(name=S.LOTTERY_UI["f_your"], value=S.LOTTERY_UI["your_none"], inline=False)
|
||||
await interaction.response.send_message(embed=embed)
|
||||
return
|
||||
|
||||
if kogus <= 0:
|
||||
await interaction.response.send_message(S.ERR["positive_amount"], ephemeral=True)
|
||||
return
|
||||
res = await economy.do_buy_ticket(interaction.user.id, kogus, period)
|
||||
if not res["ok"]:
|
||||
if res["reason"] == "db_error":
|
||||
await reply_db_error(interaction)
|
||||
elif res["reason"] == "banned":
|
||||
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||
elif res["reason"] == "max_tickets":
|
||||
await interaction.response.send_message(
|
||||
S.LOTTERY_UI["max_tickets"].format(cap=res["cap"], held=res["held"]), ephemeral=True
|
||||
)
|
||||
elif res["reason"] == "insufficient":
|
||||
await interaction.response.send_message(
|
||||
S.ERR["broke_need"].format(need=coin(res["need"])), ephemeral=True
|
||||
)
|
||||
else:
|
||||
await interaction.response.send_message(S.ERR["invalid_amount"], ephemeral=True)
|
||||
return
|
||||
await interaction.response.send_message(
|
||||
S.LOTTERY_UI["bought"].format(
|
||||
count=res["bought"], cost=coin(res["cost"]),
|
||||
tickets=res["tickets"], balance=coin(res["balance"]),
|
||||
)
|
||||
)
|
||||
|
||||
class LeaderboardView(discord.ui.View):
|
||||
PER_PAGE = 10
|
||||
|
||||
|
||||
Reference in New Issue
Block a user