add hilo, mines, and packs
This commit is contained in:
@@ -28,6 +28,9 @@ async def main():
|
|||||||
await bot.load_extension("src.cogs.coinflip")
|
await bot.load_extension("src.cogs.coinflip")
|
||||||
await bot.load_extension("src.cogs.towers")
|
await bot.load_extension("src.cogs.towers")
|
||||||
await bot.load_extension("src.cogs.baccarat")
|
await bot.load_extension("src.cogs.baccarat")
|
||||||
|
await bot.load_extension("src.cogs.hilo")
|
||||||
|
await bot.load_extension("src.cogs.mines")
|
||||||
|
await bot.load_extension("src.cogs.packs")
|
||||||
await bot.start(TOKEN)
|
await bot.start(TOKEN)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ class BlackjackPanel(discord.ui.View):
|
|||||||
e = discord.Embed(title="🎲 Blackjack", color=discord.Color.blurple())
|
e = discord.Embed(title="🎲 Blackjack", color=discord.Color.blurple())
|
||||||
e.description = (
|
e.description = (
|
||||||
"Set your **bet**, then press **Deal** to start.\n"
|
"Set your **bet**, then press **Deal** to start.\n"
|
||||||
"During a hand use **Hit / Stand / Double / Split**.\n"
|
"During a hand use **Hit / Stand / Double / Split**.\n\n"
|
||||||
f"**Current bet:** ${self.bet}\n"
|
f"**Current bet:** ${self.bet}\n"
|
||||||
)
|
)
|
||||||
e.add_field(name="Balance", value=f"${cash:,}", inline=True)
|
e.add_field(name="Balance", value=f"${cash:,}", inline=True)
|
||||||
|
|||||||
@@ -1,15 +1,36 @@
|
|||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from .. import db
|
from .. import db
|
||||||
from ..utils.constants import (
|
|
||||||
DAILY_CASH, DAILY_FREE_SPINS,
|
|
||||||
PAYOUTS, SPECIAL_SYMBOL, SPECIAL_BONUS_SPINS,
|
|
||||||
SLOTS_MIN_BET, SLOTS_MAX_BET, WILDCARD_FACTOR,
|
|
||||||
TOPUP_AMOUNT, TOPUP_COOLDOWN_MINUTES, DAILY_COOLDOWN_HOURS,
|
|
||||||
COINFLIP_MIN_CHIP, COINFLIP_MIN_BET, COINFLIP_MAX_BET, TOWERS_EDGE_PER_STEP,
|
|
||||||
)
|
|
||||||
|
|
||||||
# ---------------- Rules UI (unchanged) ----------------
|
# Try to import constants; provide safe defaults if missing.
|
||||||
|
try:
|
||||||
|
from ..utils.constants import (
|
||||||
|
DAILY_CASH, DAILY_FREE_SPINS,
|
||||||
|
PAYOUTS, SPECIAL_SYMBOL, SPECIAL_BONUS_SPINS,
|
||||||
|
SLOTS_MIN_BET, SLOTS_MAX_BET, WILDCARD_FACTOR,
|
||||||
|
TOPUP_AMOUNT, TOPUP_COOLDOWN_MINUTES, DAILY_COOLDOWN_HOURS,
|
||||||
|
COINFLIP_MIN_CHIP, COINFLIP_MIN_BET, COINFLIP_MAX_BET,
|
||||||
|
TOWERS_EDGE_PER_STEP, PACK_SIZE
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
DAILY_CASH = 10_000
|
||||||
|
DAILY_FREE_SPINS = 0
|
||||||
|
PAYOUTS = {}
|
||||||
|
SPECIAL_SYMBOL = "⭐"
|
||||||
|
SPECIAL_BONUS_SPINS = 5
|
||||||
|
SLOTS_MIN_BET = 10
|
||||||
|
SLOTS_MAX_BET = 5000
|
||||||
|
WILDCARD_FACTOR = 0.8
|
||||||
|
TOPUP_AMOUNT = 100
|
||||||
|
TOPUP_COOLDOWN_MINUTES = 5
|
||||||
|
DAILY_COOLDOWN_HOURS = 24
|
||||||
|
COINFLIP_MIN_CHIP = 10
|
||||||
|
COINFLIP_MIN_BET = 10
|
||||||
|
COINFLIP_MAX_BET = 5000
|
||||||
|
TOWERS_EDGE_PER_STEP = 0.015
|
||||||
|
PACK_SIZE = 5
|
||||||
|
|
||||||
|
# ---------------- Rules UI ----------------
|
||||||
class RulesView(discord.ui.View):
|
class RulesView(discord.ui.View):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(timeout=60)
|
super().__init__(timeout=60)
|
||||||
@@ -18,12 +39,15 @@ class RulesView(discord.ui.View):
|
|||||||
class RulesSelect(discord.ui.Select):
|
class RulesSelect(discord.ui.Select):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
options = [
|
options = [
|
||||||
discord.SelectOption(label="Blackjack", emoji="🃏", description="How to play Blackjack"),
|
discord.SelectOption(label="Blackjack", emoji="🃏", description="How to play Blackjack"),
|
||||||
discord.SelectOption(label="Slots", emoji="🎰", description="How to play Fruit Slots"),
|
discord.SelectOption(label="Slots", emoji="🎰", description="How to play Fruit Slots"),
|
||||||
discord.SelectOption(label="Roulette (Mini)", emoji="🎯", description="How to play Mini Roulette"),
|
discord.SelectOption(label="Roulette (Mini)", emoji="🎯", description="How to play Mini Roulette"),
|
||||||
discord.SelectOption(label="Coin Flip", emoji="🪙", description="How to play Coin Flip"),
|
discord.SelectOption(label="Coin Flip", emoji="🪙", description="How to play Coin Flip"),
|
||||||
discord.SelectOption(label="Towers", emoji="🐉", description="How to play Towers"),
|
discord.SelectOption(label="Towers", emoji="🐉", description="How to play Towers"),
|
||||||
discord.SelectOption(label="Baccarat", emoji="🎴", description="How to play Baccarat"),
|
discord.SelectOption(label="Baccarat", emoji="🎴", description="How to play Baccarat"),
|
||||||
|
discord.SelectOption(label="Hi-Lo", emoji="🔼", description="How to play Hi-Lo"),
|
||||||
|
discord.SelectOption(label="Mines", emoji="💣", description="Pick safe tiles, avoid bombs"),
|
||||||
|
discord.SelectOption(label="Packs", emoji="📦", description="Open 5-item packs & build a collection"),
|
||||||
]
|
]
|
||||||
super().__init__(placeholder="Choose a game…", options=options, min_values=1, max_values=1)
|
super().__init__(placeholder="Choose a game…", options=options, min_values=1, max_values=1)
|
||||||
|
|
||||||
@@ -39,20 +63,46 @@ class RulesSelect(discord.ui.Select):
|
|||||||
embed = rules_towers_embed()
|
embed = rules_towers_embed()
|
||||||
elif choice == "Baccarat":
|
elif choice == "Baccarat":
|
||||||
embed = rules_baccarat_embed()
|
embed = rules_baccarat_embed()
|
||||||
else: # Coin Flip
|
elif choice == "Hi-Lo":
|
||||||
embed = rules_coin_embed()
|
embed = rules_hilo_embed()
|
||||||
|
elif choice == "Mines":
|
||||||
|
embed = rules_mines_embed()
|
||||||
|
else: # Packs or fallback
|
||||||
|
embed = rules_packs_embed()
|
||||||
await interaction.response.edit_message(embed=embed, view=self.view)
|
await interaction.response.edit_message(embed=embed, view=self.view)
|
||||||
|
|
||||||
|
# ---- Rules embeds ----
|
||||||
|
|
||||||
|
def rules_mines_embed():
|
||||||
|
e = discord.Embed(title="💣 Mines — Rules", color=discord.Color.dark_green())
|
||||||
|
e.description = (
|
||||||
|
"A 5×5 board hides a set number of **mines** (you choose 1, 3, 5, or 7).\n"
|
||||||
|
"Start a round (bet is debited), then pick cells. Each **safe** pick grows your cashout; "
|
||||||
|
"**Cash Out** anytime. Hitting a mine loses the bet.\n\n"
|
||||||
|
"Each safe pick multiplies by roughly `total_remaining / safe_remaining` minus a small house edge."
|
||||||
|
)
|
||||||
|
return e
|
||||||
|
|
||||||
|
def rules_hilo_embed():
|
||||||
|
e = discord.Embed(title="🔼 Hi-Lo — Rules", color=discord.Color.dark_green())
|
||||||
|
e.description = (
|
||||||
|
"Guess whether the **next card** will be **Higher** or **Lower**.\n"
|
||||||
|
"• Ties **push** (no change)\n"
|
||||||
|
"• Each correct guess multiplies your cashout; **Cash Out** anytime\n"
|
||||||
|
"• Edge per step: about 2% (tunable)\n\n"
|
||||||
|
"From low cards, **Higher** pays less; from high cards, **Lower** pays less. "
|
||||||
|
"From extremes (A/K) the safe side pays ~×1."
|
||||||
|
)
|
||||||
|
return e
|
||||||
|
|
||||||
def rules_baccarat_embed():
|
def rules_baccarat_embed():
|
||||||
e = discord.Embed(title="🎴 Baccarat — Rules", color=discord.Color.dark_green())
|
e = discord.Embed(title="🎴 Baccarat — Rules", color=discord.Color.dark_green())
|
||||||
e.description = (
|
e.description = (
|
||||||
"**Bets:** Player 1:1 • Banker 1:1 (5% commission) • Tie 8:1 (Player/Banker push on tie)\n"
|
"**Bets:** Player 1:1 • Banker 1:1 (5% commission) • Tie 8:1 (Player/Banker push on tie)\n"
|
||||||
"Cards: A=1, 10/J/Q/K=0. Totals are modulo 10.\n"
|
"Cards: A=1, 10/J/Q/K=0 (mod 10 totals). Naturals (8/9) stand; otherwise standard third-card rules apply."
|
||||||
"Naturals (8/9) stand; otherwise Player/Banker draw by standard third-card rules."
|
|
||||||
)
|
)
|
||||||
return e
|
return e
|
||||||
|
|
||||||
|
|
||||||
def rules_towers_embed():
|
def rules_towers_embed():
|
||||||
e = discord.Embed(title="🐉 Towers — Rules", color=discord.Color.dark_teal())
|
e = discord.Embed(title="🐉 Towers — Rules", color=discord.Color.dark_teal())
|
||||||
e.description = (
|
e.description = (
|
||||||
@@ -62,8 +112,7 @@ def rules_towers_embed():
|
|||||||
"• **Hard:** 2 / 1\n"
|
"• **Hard:** 2 / 1\n"
|
||||||
"• **Expert:** 3 / 2\n"
|
"• **Expert:** 3 / 2\n"
|
||||||
"• **Master:** 4 / 3\n\n"
|
"• **Master:** 4 / 3\n\n"
|
||||||
"You pay your bet once on the first pick. Each safe pick multiplies your cashout; "
|
f"You pay your bet once on the first pick. Each safe pick multiplies your cashout (edge ~{int(TOWERS_EDGE_PER_STEP*100)}% per step). "
|
||||||
f"a small house edge (~{int(TOWERS_EDGE_PER_STEP*100)}% per step) is applied. "
|
|
||||||
"Cash out anytime; hit a bad tile and you lose the bet."
|
"Cash out anytime; hit a bad tile and you lose the bet."
|
||||||
)
|
)
|
||||||
return e
|
return e
|
||||||
@@ -71,9 +120,9 @@ def rules_towers_embed():
|
|||||||
def rules_coin_embed():
|
def rules_coin_embed():
|
||||||
e = discord.Embed(title="🪙 Coin Flip — Rules", color=discord.Color.blurple())
|
e = discord.Embed(title="🪙 Coin Flip — Rules", color=discord.Color.blurple())
|
||||||
e.description = (
|
e.description = (
|
||||||
"Pick **Heads** or **Tails**, set your **Chip**, and flip.\n"
|
"Pick **Heads** or **Tails**, set your **Bet**, and flip.\n"
|
||||||
"**Payout:** 1:1 • "
|
f"**Payout:** 1:1 • **Limits:** per flip ${COINFLIP_MIN_BET}–${COINFLIP_MAX_BET} "
|
||||||
f"**Limits:** min chip ${COINFLIP_MIN_CHIP}, per flip ${COINFLIP_MIN_BET}–${COINFLIP_MAX_BET}"
|
f"(min chip ${COINFLIP_MIN_CHIP})."
|
||||||
)
|
)
|
||||||
return e
|
return e
|
||||||
|
|
||||||
@@ -82,24 +131,28 @@ def rules_blackjack_embed():
|
|||||||
e.description = (
|
e.description = (
|
||||||
"**Objective:** Get as close to 21 as possible without going over.\n\n"
|
"**Objective:** Get as close to 21 as possible without going over.\n\n"
|
||||||
"**Card Values:** Numbers=face value, J/Q/K=10, A=11 or 1.\n\n"
|
"**Card Values:** Numbers=face value, J/Q/K=10, A=11 or 1.\n\n"
|
||||||
"**Actions:** Hit, Stand, Double Down (first two cards), Split (matching ranks/10-values).\n"
|
"**Actions:** Hit, Stand, Double Down (first two cards), Split (matching ranks/10-values). "
|
||||||
"Dealer hits to 16, stands on 17.\n\n"
|
"Dealer hits to 16, stands on 17.\n\n"
|
||||||
"**Payouts:** Win=1:1, Blackjack=3:2, Push=bet returned."
|
"**Payouts:** Win=1:1, Blackjack=3:2, Push=bet returned."
|
||||||
)
|
)
|
||||||
return e
|
return e
|
||||||
|
|
||||||
def rules_slots_embed():
|
def rules_slots_embed():
|
||||||
pairs = sorted(PAYOUTS.items(), key=lambda kv: kv[1], reverse=True)
|
# Payout table (if provided) to pretty-print
|
||||||
lines = [f"{sym} x{mult}" for sym, mult in pairs]
|
if PAYOUTS:
|
||||||
table = "```\nSymbol Multiplier\n" + "\n".join(lines) + "\n```"
|
pairs = sorted(PAYOUTS.items(), key=lambda kv: kv[1], reverse=True)
|
||||||
|
lines = [f"{sym} x{mult}" for sym, mult in pairs]
|
||||||
|
table = "```\nSymbol Multiplier\n" + "\n".join(lines) + "\n```"
|
||||||
|
else:
|
||||||
|
table = "—"
|
||||||
|
|
||||||
e = discord.Embed(title="🎰 Fruit Slots — Rules", color=discord.Color.purple())
|
e = discord.Embed(title="🎰 Fruit Slots — Rules", color=discord.Color.purple())
|
||||||
e.description = (
|
e.description = (
|
||||||
"**Board:** 3×3 grid. Wins on rows, columns, and diagonals.\n"
|
"**Board:** 3×3 grid. Wins on rows, columns, and diagonals.\n"
|
||||||
f"**Bet:** `!slots <bet>` (min ${SLOTS_MIN_BET}, max ${SLOTS_MAX_BET}).\n"
|
f"**Bet:** set on the panel (min ${SLOTS_MIN_BET}, max ${SLOTS_MAX_BET}).\n"
|
||||||
"**Paylines:** Choose **1 / 3 / 5 / 8** lines on the message. Your **spin bet** is split evenly across active lines.\n"
|
"**Paylines:** Choose **1 / 3 / 5 / 8** lines. Your **spin bet** is split evenly across active lines.\n"
|
||||||
"• Example: Bet $80 on 8 lines → $10 per line; a line paying x9 returns $90.\n\n"
|
"• Example: Bet $80 on 8 lines → $10 per line; a line paying x9 returns $90.\n\n"
|
||||||
f"**Wildcard:** ⭐ counts as any fruit only for lines with **exactly two of the same fruit + one ⭐**; pays **{int(WILDCARD_FACTOR*100)}%** of that fruit's multiplier.\n"
|
f"**Wildcard:** ⭐ acts as any fruit only for lines with **exactly two of the same fruit + one ⭐**; pays **{int(WILDCARD_FACTOR*100)}%** of that fruit's multiplier.\n"
|
||||||
f"**Bonus:** {SPECIAL_SYMBOL}{SPECIAL_SYMBOL}{SPECIAL_SYMBOL} on the **middle row** awards **{SPECIAL_BONUS_SPINS} free spins** (only if that line is active).\n"
|
f"**Bonus:** {SPECIAL_SYMBOL}{SPECIAL_SYMBOL}{SPECIAL_SYMBOL} on the **middle row** awards **{SPECIAL_BONUS_SPINS} free spins** (only if that line is active).\n"
|
||||||
"**Payouts:** Three-in-a-row multipliers (applied per winning line × per-line bet)."
|
"**Payouts:** Three-in-a-row multipliers (applied per winning line × per-line bet)."
|
||||||
)
|
)
|
||||||
@@ -113,11 +166,22 @@ def rules_roulette_mini_embed():
|
|||||||
"**Outside Bets:** Red/Black 1:1 • Even/Odd 1:1 (0 loses) • Low/High 1:1 • "
|
"**Outside Bets:** Red/Black 1:1 • Even/Odd 1:1 (0 loses) • Low/High 1:1 • "
|
||||||
"Column (6 nums) 2:1 • Sixes (1–6 / 7–12 / 13–18) 2:1\n"
|
"Column (6 nums) 2:1 • Sixes (1–6 / 7–12 / 13–18) 2:1\n"
|
||||||
"**Inside Bets:** Straight 17:1 • Split 8:1 • Street 5:1 • Six-line 2:1\n\n"
|
"**Inside Bets:** Straight 17:1 • Split 8:1 • Street 5:1 • Six-line 2:1\n\n"
|
||||||
"Use **Add Outside/Inside** to build the slip, set **Chip**, then **Spin** or **Spin Again**."
|
"Use the panel to build bets, set **Bet**, then **Spin** / **Spin Again**."
|
||||||
)
|
)
|
||||||
return e
|
return e
|
||||||
|
|
||||||
# ---------------- Casino menu ----------------
|
def rules_packs_embed():
|
||||||
|
e = discord.Embed(title="📦 Packs — Rules", color=discord.Color.dark_gold())
|
||||||
|
e.description = (
|
||||||
|
f"Open a pack of **{PACK_SIZE}** items. Each item has a **rarity** and a **payout multiplier**.\n"
|
||||||
|
"Your return is the **sum of multipliers × your bet**. New items are added to your **collection**.\n\n"
|
||||||
|
"**Rarities:** Common, Uncommon, Rare, Epic, Legendary, Mythic (rarest).\n"
|
||||||
|
"Use **Set Bet**, **×2**, **½**, then **Open Pack**. Use **Open Again** to repeat quickly.\n"
|
||||||
|
"Click **Collection** on the panel to see progress."
|
||||||
|
)
|
||||||
|
return e
|
||||||
|
|
||||||
|
# ---------------- Casino launcher ----------------
|
||||||
class CasinoView(discord.ui.View):
|
class CasinoView(discord.ui.View):
|
||||||
"""Buttons to launch any game for the invoking user."""
|
"""Buttons to launch any game for the invoking user."""
|
||||||
def __init__(self, cog: "Economy", ctx: commands.Context):
|
def __init__(self, cog: "Economy", ctx: commands.Context):
|
||||||
@@ -133,17 +197,16 @@ class CasinoView(discord.ui.View):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
async def _launch(self, interaction: discord.Interaction, command_name: str):
|
async def _launch(self, interaction: discord.Interaction, command_name: str):
|
||||||
# Acknowledge the click fast so Discord doesn't show "interaction failed"
|
# Acknowledge fast
|
||||||
try:
|
try:
|
||||||
await interaction.response.defer(thinking=False)
|
await interaction.response.defer(thinking=False)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Delete the casino menu message to avoid clutter
|
# Delete the menu to reduce clutter
|
||||||
try:
|
try:
|
||||||
await interaction.message.delete()
|
await interaction.message.delete()
|
||||||
except Exception:
|
except Exception:
|
||||||
# Fallback: disable the view & mark as launching
|
|
||||||
for item in self.children:
|
for item in self.children:
|
||||||
item.disabled = True
|
item.disabled = True
|
||||||
try:
|
try:
|
||||||
@@ -151,32 +214,40 @@ class CasinoView(discord.ui.View):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Now invoke the actual game command
|
# Invoke command
|
||||||
cmd = self.cog.bot.get_command(command_name)
|
cmd = self.cog.bot.get_command(command_name)
|
||||||
if cmd is None:
|
if cmd is None:
|
||||||
return await self.ctx.send(f"⚠️ Command `{command_name}` not found.")
|
return await self.ctx.send(f"⚠️ Command `{command_name}` not found.")
|
||||||
await self.ctx.invoke(cmd)
|
await self.ctx.invoke(cmd)
|
||||||
|
|
||||||
|
@discord.ui.button(label="Blackjack", style=discord.ButtonStyle.success, emoji="🃏", row=0)
|
||||||
|
async def bj(self, itx: discord.Interaction, _): await self._launch(itx, "blackjack")
|
||||||
|
|
||||||
@discord.ui.button(label="Blackjack", style=discord.ButtonStyle.success, emoji="🃏", row=0)
|
@discord.ui.button(label="Slots", style=discord.ButtonStyle.primary, emoji="🎰", row=0)
|
||||||
async def bj(self, itx: discord.Interaction, _): await self._launch(itx, "blackjack")
|
async def slots(self, itx: discord.Interaction, _): await self._launch(itx, "slots")
|
||||||
|
|
||||||
@discord.ui.button(label="Slots", style=discord.ButtonStyle.primary, emoji="🎰", row=0)
|
@discord.ui.button(label="Mini Roulette", style=discord.ButtonStyle.secondary,emoji="🎯", row=1)
|
||||||
async def slots(self, itx: discord.Interaction, _): await self._launch(itx, "slots")
|
async def roulette(self, itx: discord.Interaction, _): await self._launch(itx, "roulette")
|
||||||
|
|
||||||
@discord.ui.button(label="Mini Roulette", style=discord.ButtonStyle.secondary, emoji="🎯", row=1)
|
@discord.ui.button(label="Coin Flip", style=discord.ButtonStyle.secondary,emoji="🪙", row=1)
|
||||||
async def roulette(self, itx: discord.Interaction, _): await self._launch(itx, "roulette")
|
async def coin(self, itx: discord.Interaction, _): await self._launch(itx, "coin")
|
||||||
|
|
||||||
@discord.ui.button(label="Coin Flip", style=discord.ButtonStyle.secondary, emoji="🪙", row=1)
|
@discord.ui.button(label="Towers", style=discord.ButtonStyle.secondary,emoji="🐉", row=2)
|
||||||
async def coin(self, itx: discord.Interaction, _): await self._launch(itx, "coin")
|
async def towers(self, itx: discord.Interaction, _): await self._launch(itx, "towers")
|
||||||
|
|
||||||
@discord.ui.button(label="Towers", style=discord.ButtonStyle.secondary, emoji="🐉", row=2)
|
@discord.ui.button(label="Baccarat", style=discord.ButtonStyle.secondary,emoji="🎴", row=2)
|
||||||
async def towers(self, itx: discord.Interaction, _): await self._launch(itx, "towers")
|
async def baccarat(self, itx: discord.Interaction, _): await self._launch(itx, "baccarat")
|
||||||
|
|
||||||
@discord.ui.button(label="Baccarat", style=discord.ButtonStyle.secondary, emoji="🎴", row=2)
|
@discord.ui.button(label="Hi-Lo", style=discord.ButtonStyle.secondary,emoji="🔼", row=2)
|
||||||
async def baccarat(self, itx: discord.Interaction, _): await self._launch(itx, "baccarat")
|
async def hilo(self, itx: discord.Interaction, _): await self._launch(itx, "hilo")
|
||||||
|
|
||||||
|
@discord.ui.button(label="Mines", style=discord.ButtonStyle.secondary,emoji="💣", row=2)
|
||||||
|
async def mines(self, itx: discord.Interaction, _): await self._launch(itx, "mines")
|
||||||
|
|
||||||
|
@discord.ui.button(label="Packs", style=discord.ButtonStyle.secondary,emoji="📦", row=3)
|
||||||
|
async def packs(self, itx: discord.Interaction, _): await self._launch(itx, "packs")
|
||||||
|
|
||||||
|
# ---------------- Economy cog ----------------
|
||||||
class Economy(commands.Cog):
|
class Economy(commands.Cog):
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
@@ -222,16 +293,21 @@ class Economy(commands.Cog):
|
|||||||
if target.avatar:
|
if target.avatar:
|
||||||
embed.set_thumbnail(url=target.avatar.url)
|
embed.set_thumbnail(url=target.avatar.url)
|
||||||
|
|
||||||
# Topline economy
|
# Topline
|
||||||
embed.add_field(name="Cash", value=f"${s['cash']}", inline=True)
|
embed.add_field(name="Cash", value=f"${s['cash']}", inline=True)
|
||||||
embed.add_field(name="Free Spins", value=f"{s['free_spins']}", inline=True)
|
embed.add_field(name="Free Spins", value=f"{s['free_spins']}", inline=True)
|
||||||
embed.add_field(name="Overall W/L", value=f"{s['games_won']}/{s['games_played']} ({pct(s['games_won'], s['games_played'])})", inline=True)
|
embed.add_field(name="Overall W/L", value=f"{s['games_won']}/{s['games_played']} ({pct(s['games_won'], s['games_played'])})", inline=True)
|
||||||
|
|
||||||
|
# Per-game
|
||||||
embed.add_field(name="🃏 Blackjack", value=f"W/L: **{s['gw_bj']}/{s['gp_bj']}** ({pct(s['gw_bj'], s['gp_bj'])})", inline=True)
|
embed.add_field(name="🃏 Blackjack", value=f"W/L: **{s['gw_bj']}/{s['gp_bj']}** ({pct(s['gw_bj'], s['gp_bj'])})", inline=True)
|
||||||
embed.add_field(name="🎰 Slots", value=f"W/L: **{s['gw_slots']}/{s['gp_slots']}** ({pct(s['gw_slots'], s['gp_slots'])})", inline=True)
|
embed.add_field(name="🎰 Slots", value=f"W/L: **{s['gw_slots']}/{s['gp_slots']}** ({pct(s['gw_slots'], s['gp_slots'])})", inline=True)
|
||||||
embed.add_field(name="🎯 Roulette", value=f"Spins: **{s['gp_roulette']}** • Net: **{'+' if s['roulette_net']>=0 else ''}${s['roulette_net']}**", inline=True)
|
embed.add_field(name="🎯 Roulette", value=f"Spins: **{s['gp_roulette']}** • Net: **{'+' if s['roulette_net']>=0 else ''}${s['roulette_net']}**", inline=True)
|
||||||
embed.add_field(name="🪙 Coin Flip", value=f"W/L: **{s['gw_coin']}/{s['gp_coin']}** ({pct(s['gw_coin'], s['gp_coin'])})", inline=True)
|
embed.add_field(name="🪙 Coin Flip", value=f"W/L: **{s['gw_coin']}/{s['gp_coin']}** ({pct(s['gw_coin'], s['gp_coin'])})", inline=True)
|
||||||
embed.add_field(name="🐉 Towers", value=f"W/L: **{s['gw_towers']}/{s['gp_towers']}** ({pct(s['gw_towers'], s['gp_towers'])})", inline=True)
|
embed.add_field(name="🐉 Towers", value=f"W/L: **{s['gw_towers']}/{s['gp_towers']}** ({pct(s['gw_towers'], s['gp_towers'])})", inline=True)
|
||||||
embed.add_field(name="🎴 Baccarat", value=f"Hands: **{s['gp_bac']}** • Net: **{'+' if s['bac_net']>=0 else ''}${s['bac_net']}**", inline=True)
|
embed.add_field(name="🎴 Baccarat", value=f"Hands: **{s['gp_bac']}** • Net: **{'+' if s['bac_net']>=0 else ''}${s['bac_net']}**", inline=True)
|
||||||
|
embed.add_field(name="🔼 Hi-Lo", value=f"Rounds: **{s['gp_hilo']}** • Net: **{'+' if s['hilo_net']>=0 else ''}${s['hilo_net']}**", inline=True)
|
||||||
|
embed.add_field(name="💣 Mines", value=f"Rounds: **{s['gp_mines']}** • Net: **{'+' if s['mines_net']>=0 else ''}${s['mines_net']}**", inline=True)
|
||||||
|
embed.add_field(name="📦 Packs", value=f"Opened: **{s['gp_packs']}** • Net: **{'+' if s['packs_net']>=0 else ''}${s['packs_net']}**", inline=True)
|
||||||
|
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
@@ -241,14 +317,13 @@ class Economy(commands.Cog):
|
|||||||
can, secs_left = db.daily_cooldown(user_id, DAILY_COOLDOWN_HOURS)
|
can, secs_left = db.daily_cooldown(user_id, DAILY_COOLDOWN_HOURS)
|
||||||
if not can:
|
if not can:
|
||||||
h = secs_left // 3600; m = (secs_left % 3600) // 60; s = secs_left % 60
|
h = secs_left // 3600; m = (secs_left % 3600) // 60; s = secs_left % 60
|
||||||
await ctx.send(f"⏳ You can claim again in **{h}h {m}m {s}s**.")
|
return await ctx.send(f"⏳ You can claim again in **{h}h {m}m {s}s**.")
|
||||||
return
|
|
||||||
db.claim_daily(user_id, DAILY_CASH, DAILY_FREE_SPINS)
|
db.claim_daily(user_id, DAILY_CASH, DAILY_FREE_SPINS)
|
||||||
stats = db.get_full_stats(user_id)
|
stats = db.get_full_stats(user_id)
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
title="🎁 Daily Bonus!",
|
title="🎁 Daily Bonus!",
|
||||||
description=(f"You received **${DAILY_CASH}** and **{DAILY_FREE_SPINS} free spins**!\n"
|
description=(f"You received **${DAILY_CASH}** and **{DAILY_FREE_SPINS} free spins**!\n"
|
||||||
f"New balance: **${stats['cash']}**, Free spins: **{stats['free_spins']}**"),
|
f"New balance: **${stats['cash']}**, Free spins: **${stats['free_spins']}**"),
|
||||||
color=discord.Color.green()
|
color=discord.Color.green()
|
||||||
)
|
)
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
@@ -256,7 +331,8 @@ class Economy(commands.Cog):
|
|||||||
@commands.command(name="leaderboard", aliases=["lb"])
|
@commands.command(name="leaderboard", aliases=["lb"])
|
||||||
async def leaderboard(self, ctx):
|
async def leaderboard(self, ctx):
|
||||||
rows = db.top_cash(10)
|
rows = db.top_cash(10)
|
||||||
if not rows: return await ctx.send("No players found!")
|
if not rows:
|
||||||
|
return await ctx.send("No players found!")
|
||||||
lines = []
|
lines = []
|
||||||
for i, (uid, cash) in enumerate(rows, 1):
|
for i, (uid, cash) in enumerate(rows, 1):
|
||||||
name = await self.resolve_name(ctx, uid)
|
name = await self.resolve_name(ctx, uid)
|
||||||
@@ -311,6 +387,7 @@ class Economy(commands.Cog):
|
|||||||
if err == "INSUFFICIENT_FUNDS": return await ctx.send(f"💸 Not enough cash. Your balance: ${from_cash}")
|
if err == "INSUFFICIENT_FUNDS": return await ctx.send(f"💸 Not enough cash. Your balance: ${from_cash}")
|
||||||
if err == "SAME_USER": return await ctx.send("🪞 You can’t tip yourself.")
|
if err == "SAME_USER": return await ctx.send("🪞 You can’t tip yourself.")
|
||||||
return await ctx.send("❌ Couldn’t complete the tip. Try again in a moment.")
|
return await ctx.send("❌ Couldn’t complete the tip. Try again in a moment.")
|
||||||
|
# names
|
||||||
sender = await self.resolve_name(ctx, ctx.author.id)
|
sender = await self.resolve_name(ctx, ctx.author.id)
|
||||||
recv = await self.resolve_name(ctx, member.id)
|
recv = await self.resolve_name(ctx, member.id)
|
||||||
desc = f"**{sender}** → **{recv}**\nAmount: **${amt:,}**"
|
desc = f"**{sender}** → **{recv}**\nAmount: **${amt:,}**"
|
||||||
@@ -322,4 +399,3 @@ class Economy(commands.Cog):
|
|||||||
|
|
||||||
async def setup(bot):
|
async def setup(bot):
|
||||||
await bot.add_cog(Economy(bot))
|
await bot.add_cog(Economy(bot))
|
||||||
|
|
||||||
|
|||||||
357
src/cogs/hilo.py
Normal file
357
src/cogs/hilo.py
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
# src/cogs/hilo.py
|
||||||
|
import random
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from .. import db
|
||||||
|
|
||||||
|
# ---- Config (safe fallbacks; override in constants.py if you like) ----
|
||||||
|
try:
|
||||||
|
from ..utils.constants import HILO_EDGE_PER_STEP, HILO_MAX_MULT, HILO_MIN_BET
|
||||||
|
except Exception:
|
||||||
|
HILO_EDGE_PER_STEP = 0.02 # 2% house edge per correct step
|
||||||
|
HILO_MAX_MULT = 50.0 # cap total multiplier
|
||||||
|
HILO_MIN_BET = 10
|
||||||
|
|
||||||
|
SUITS = ["♠️","♥️","♦️","♣️"]
|
||||||
|
RANKS = ["A","2","3","4","5","6","7","8","9","10","J","Q","K"]
|
||||||
|
RANK_TO_VAL = {r:i+1 for i,r in enumerate(RANKS)} # A=1 ... K=13
|
||||||
|
|
||||||
|
def draw_card():
|
||||||
|
return random.choice(RANKS), random.choice(SUITS)
|
||||||
|
|
||||||
|
def step_multiplier(cur_rank_val: int, direction: str) -> float:
|
||||||
|
"""
|
||||||
|
Ties are 'push' (no change), so effective trial space ignores equals.
|
||||||
|
Wins among 12 non-equal ranks.
|
||||||
|
- Higher: wins = 13 - cur
|
||||||
|
- Lower: wins = cur - 1
|
||||||
|
Fair step mult = 12 / wins. Apply house edge. Clamp at 1.0 for guaranteed steps.
|
||||||
|
"""
|
||||||
|
if direction == "up":
|
||||||
|
wins = 13 - cur_rank_val
|
||||||
|
else:
|
||||||
|
wins = cur_rank_val - 1
|
||||||
|
if wins <= 0:
|
||||||
|
return 1.0
|
||||||
|
fair = 12.0 / wins
|
||||||
|
mult = fair * (1.0 - HILO_EDGE_PER_STEP)
|
||||||
|
return max(1.0, mult)
|
||||||
|
|
||||||
|
def card_str(rank: str, suit: str) -> str:
|
||||||
|
return f"{rank}{suit}"
|
||||||
|
|
||||||
|
def fmt_money(n: int) -> str:
|
||||||
|
return f"${n:,}"
|
||||||
|
|
||||||
|
class SetBetModal(discord.ui.Modal, title="Set Hi-Lo Bet"):
|
||||||
|
def __init__(self, view: "HiloView"):
|
||||||
|
super().__init__()
|
||||||
|
self.view = view
|
||||||
|
self.amount = discord.ui.TextInput(
|
||||||
|
label=f"Amount (min {HILO_MIN_BET})",
|
||||||
|
placeholder=str(self.view.bet),
|
||||||
|
required=True, min_length=1, max_length=10,
|
||||||
|
)
|
||||||
|
self.add_item(self.amount)
|
||||||
|
|
||||||
|
async def on_submit(self, itx: discord.Interaction):
|
||||||
|
if itx.user.id != self.view.user_id:
|
||||||
|
return await itx.response.send_message("This isn’t your Hi-Lo panel.", ephemeral=True)
|
||||||
|
if self.view.in_round:
|
||||||
|
return await itx.response.send_message("You can’t change bet during a round.", ephemeral=True)
|
||||||
|
raw = str(self.amount.value)
|
||||||
|
try:
|
||||||
|
amt = int("".join(ch for ch in raw if ch.isdigit()))
|
||||||
|
except Exception:
|
||||||
|
return await itx.response.send_message("Enter a valid number.", ephemeral=True)
|
||||||
|
self.view.bet = max(HILO_MIN_BET, amt)
|
||||||
|
await itx.response.defer(ephemeral=True)
|
||||||
|
if self.view.message:
|
||||||
|
await self.view.message.edit(embed=self.view.render(), view=self.view)
|
||||||
|
|
||||||
|
class HiloView(discord.ui.View):
|
||||||
|
"""
|
||||||
|
Row 0: Set Bet · ×2 · ½ · Start
|
||||||
|
Row 1: LOWER · Cash Out · HIGHER
|
||||||
|
"""
|
||||||
|
def __init__(self, user_id: int, initial_bet: int = 50, *, timeout: int = 180):
|
||||||
|
super().__init__(timeout=timeout)
|
||||||
|
self.user_id = user_id
|
||||||
|
self.bet = max(HILO_MIN_BET, initial_bet)
|
||||||
|
|
||||||
|
# round state
|
||||||
|
self.in_round = False
|
||||||
|
self.current_card: Optional[tuple[str,str]] = None
|
||||||
|
self.mult: float = 1.0
|
||||||
|
self.steps: int = 0
|
||||||
|
self.last_note: Optional[str] = None
|
||||||
|
self.message: Optional[discord.Message] = None
|
||||||
|
|
||||||
|
# NEW: summary of the last finished round (loss or cashout)
|
||||||
|
# keys: 'from','to','guess','net','returned','steps','mult'
|
||||||
|
self.last_summary: Optional[dict] = None
|
||||||
|
|
||||||
|
self._busy = False
|
||||||
|
self._sync_buttons()
|
||||||
|
|
||||||
|
def disable_all(self):
|
||||||
|
for c in self.children:
|
||||||
|
c.disabled = True
|
||||||
|
|
||||||
|
async def on_timeout(self):
|
||||||
|
self.disable_all()
|
||||||
|
try:
|
||||||
|
if self.message: await self.message.edit(view=self)
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
# ----- rendering -----
|
||||||
|
def render(self) -> discord.Embed:
|
||||||
|
cash, _ = db.get_wallet(self.user_id)
|
||||||
|
|
||||||
|
# Pick color based on state / last result
|
||||||
|
color = discord.Color.blurple()
|
||||||
|
if self.in_round:
|
||||||
|
color = discord.Color.green()
|
||||||
|
elif self.last_summary is not None:
|
||||||
|
net = self.last_summary.get("net", 0)
|
||||||
|
if net > 0: color = discord.Color.green()
|
||||||
|
elif net < 0: color = discord.Color.red()
|
||||||
|
else: color = discord.Color.orange()
|
||||||
|
|
||||||
|
e = discord.Embed(title="🔼 Hi-Lo", color=color)
|
||||||
|
|
||||||
|
if not self.in_round or not self.current_card:
|
||||||
|
desc = [
|
||||||
|
"Set your **bet**, press **Start**, then guess **Higher** or **Lower**.",
|
||||||
|
"Ties **push** (no gain/loss). **Cash Out** anytime.",
|
||||||
|
"",
|
||||||
|
f"**Current bet:** {fmt_money(self.bet)}",
|
||||||
|
f"**Balance:** {fmt_money(cash)}",
|
||||||
|
]
|
||||||
|
# Show last round summary if available
|
||||||
|
if self.last_summary is not None:
|
||||||
|
sym = "💰" if self.last_summary["net"] > 0 else ("❌" if self.last_summary["net"] < 0 else "↔️")
|
||||||
|
guess = self.last_summary.get("guess", "")
|
||||||
|
fromc = self.last_summary.get("from")
|
||||||
|
toc = self.last_summary.get("to")
|
||||||
|
steps = self.last_summary.get("steps", 0)
|
||||||
|
mult = self.last_summary.get("mult", 1.0)
|
||||||
|
returned = self.last_summary.get("returned", 0)
|
||||||
|
net = self.last_summary.get("net", 0)
|
||||||
|
|
||||||
|
line1 = f"**Last Result:** {sym} " \
|
||||||
|
f"{'Cashed out' if guess=='CASHOUT' else f'Guess {guess}'}"
|
||||||
|
if fromc and toc:
|
||||||
|
line1 += f" — {card_str(*fromc)} → {card_str(*toc)}"
|
||||||
|
elif fromc:
|
||||||
|
line1 += f" — start {card_str(*fromc)}"
|
||||||
|
|
||||||
|
line2 = f"Steps: **{steps}**, Mult: **×{mult:.2f}**, " \
|
||||||
|
f"Returned: **{fmt_money(returned)}**, Net: **{'+' if net>0 else ''}{fmt_money(net)}**"
|
||||||
|
desc += ["", line1, line2]
|
||||||
|
|
||||||
|
e.description = "\n".join(desc)
|
||||||
|
return e
|
||||||
|
|
||||||
|
# In-round panel
|
||||||
|
rank, suit = self.current_card
|
||||||
|
cur_val = RANK_TO_VAL[rank]
|
||||||
|
up_mult = step_multiplier(cur_val, "up")
|
||||||
|
dn_mult = step_multiplier(cur_val, "down")
|
||||||
|
|
||||||
|
e.add_field(name="Current Card", value=f"**{card_str(rank, suit)}**", inline=True)
|
||||||
|
e.add_field(name="Steps", value=str(self.steps), inline=True)
|
||||||
|
e.add_field(name="Multiplier", value=f"x{self.mult:.2f}", inline=True)
|
||||||
|
e.add_field(name="Potential Cashout", value=fmt_money(int(self.bet * self.mult)), inline=True)
|
||||||
|
e.add_field(name="Next Step (Higher)", value=f"×{up_mult:.2f}", inline=True)
|
||||||
|
e.add_field(name="Next Step (Lower)", value=f"×{dn_mult:.2f}", inline=True)
|
||||||
|
|
||||||
|
if self.last_note:
|
||||||
|
e.set_footer(text=self.last_note)
|
||||||
|
return e
|
||||||
|
|
||||||
|
# ----- helpers -----
|
||||||
|
def _sync_buttons(self):
|
||||||
|
hand_active = self.in_round and self.current_card is not None
|
||||||
|
# Row 0
|
||||||
|
for lbl in ("Set Bet","×2","½","Start"):
|
||||||
|
b = self._btn(lbl)
|
||||||
|
if b: b.disabled = hand_active
|
||||||
|
# Row 1
|
||||||
|
for lbl in ("LOWER","Cash Out","HIGHER"):
|
||||||
|
b = self._btn(lbl)
|
||||||
|
if b: b.disabled = not hand_active
|
||||||
|
|
||||||
|
# Edge cases: disable impossible guesses
|
||||||
|
if hand_active and self.current_card:
|
||||||
|
cur = RANK_TO_VAL[self.current_card[0]]
|
||||||
|
if self._btn("LOWER"):
|
||||||
|
self._btn("LOWER").disabled = self._btn("LOWER").disabled or (cur == 1)
|
||||||
|
if self._btn("HIGHER"):
|
||||||
|
self._btn("HIGHER").disabled = self._btn("HIGHER").disabled or (cur == 13)
|
||||||
|
|
||||||
|
def _btn(self, label):
|
||||||
|
for c in self.children:
|
||||||
|
if isinstance(c, discord.ui.Button) and c.label == label:
|
||||||
|
return c
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _reset_round(self):
|
||||||
|
self.in_round = False
|
||||||
|
self.current_card = None
|
||||||
|
self.mult = 1.0
|
||||||
|
self.steps = 0
|
||||||
|
self.last_note = None
|
||||||
|
self._sync_buttons()
|
||||||
|
|
||||||
|
async def _start_round(self, itx: discord.Interaction):
|
||||||
|
# debit bet up front
|
||||||
|
cash, _ = db.get_wallet(self.user_id)
|
||||||
|
if self.bet > cash:
|
||||||
|
return await itx.response.send_message("Not enough cash for that bet.", ephemeral=True)
|
||||||
|
|
||||||
|
db.add_cash(self.user_id, -self.bet)
|
||||||
|
self.in_round = True
|
||||||
|
self.mult = 1.0
|
||||||
|
self.steps = 0
|
||||||
|
self.last_note = None
|
||||||
|
self.current_card = draw_card()
|
||||||
|
self._sync_buttons()
|
||||||
|
await itx.response.edit_message(embed=self.render(), view=self)
|
||||||
|
|
||||||
|
async def _resolve_step(self, itx: discord.Interaction, direction: str):
|
||||||
|
if self._busy: return
|
||||||
|
self._busy = True
|
||||||
|
|
||||||
|
from_rank, from_suit = self.current_card
|
||||||
|
cur_val = RANK_TO_VAL[from_rank]
|
||||||
|
|
||||||
|
# draw until not equal (ties push)
|
||||||
|
while True:
|
||||||
|
nrank, nsuit = draw_card()
|
||||||
|
if nrank != from_rank:
|
||||||
|
break
|
||||||
|
# gentle note, no state change
|
||||||
|
self.last_note = "Tie (same rank) — try again."
|
||||||
|
await itx.response.edit_message(embed=self.render(), view=self)
|
||||||
|
self._busy = False
|
||||||
|
return # require another click to proceed
|
||||||
|
|
||||||
|
win = (RANK_TO_VAL[nrank] > cur_val) if direction == "up" else (RANK_TO_VAL[nrank] < cur_val)
|
||||||
|
|
||||||
|
if win:
|
||||||
|
# multiply and advance
|
||||||
|
self.steps += 1
|
||||||
|
self.mult *= step_multiplier(cur_val, "up" if direction == "up" else "down")
|
||||||
|
self.mult = min(self.mult, HILO_MAX_MULT)
|
||||||
|
self.current_card = (nrank, nsuit)
|
||||||
|
self.last_note = "✅ Correct! Cash out anytime."
|
||||||
|
self._sync_buttons()
|
||||||
|
try:
|
||||||
|
await itx.response.edit_message(embed=self.render(), view=self)
|
||||||
|
finally:
|
||||||
|
self._busy = False
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# lose round: no return, bet already debited
|
||||||
|
returned = 0
|
||||||
|
db.record_hilo(self.user_id, bet=self.bet, return_amount=returned, won=False)
|
||||||
|
# Save a visible summary BEFORE resetting state
|
||||||
|
self.last_summary = {
|
||||||
|
"from": (from_rank, from_suit),
|
||||||
|
"to": (nrank, nsuit),
|
||||||
|
"guess": "HIGHER" if direction == "up" else "LOWER",
|
||||||
|
"returned": returned,
|
||||||
|
"net": -self.bet,
|
||||||
|
"steps": self.steps,
|
||||||
|
"mult": self.mult,
|
||||||
|
}
|
||||||
|
self._reset_round()
|
||||||
|
await itx.response.edit_message(
|
||||||
|
embed=self.render(), view=self
|
||||||
|
)
|
||||||
|
self._busy = False
|
||||||
|
return
|
||||||
|
|
||||||
|
async def _cashout(self, itx: discord.Interaction):
|
||||||
|
if not self.in_round:
|
||||||
|
return await itx.response.send_message("No active round.", ephemeral=True)
|
||||||
|
returned = int(self.bet * self.mult)
|
||||||
|
if returned > 0:
|
||||||
|
db.add_cash(self.user_id, returned)
|
||||||
|
db.record_hilo(self.user_id, bet=self.bet, return_amount=returned, won=(returned > self.bet))
|
||||||
|
|
||||||
|
# Save summary for post-round panel
|
||||||
|
self.last_summary = {
|
||||||
|
"from": self.current_card,
|
||||||
|
"to": None,
|
||||||
|
"guess": "CASHOUT",
|
||||||
|
"returned": returned,
|
||||||
|
"net": returned - self.bet,
|
||||||
|
"steps": self.steps,
|
||||||
|
"mult": self.mult,
|
||||||
|
}
|
||||||
|
self._reset_round()
|
||||||
|
await itx.response.edit_message(embed=self.render(), view=self)
|
||||||
|
|
||||||
|
# ----- Row 0 -----
|
||||||
|
@discord.ui.button(label="Set Bet", style=discord.ButtonStyle.secondary, row=0)
|
||||||
|
async def set_bet(self, itx: discord.Interaction, _):
|
||||||
|
if itx.user.id != self.user_id: return await itx.response.send_message("This isn’t your Hi-Lo panel.", ephemeral=True)
|
||||||
|
if self.in_round: return await itx.response.send_message("You can’t change bet during a round.", ephemeral=True)
|
||||||
|
await itx.response.send_modal(SetBetModal(self))
|
||||||
|
|
||||||
|
@discord.ui.button(label="×2", style=discord.ButtonStyle.secondary, row=0)
|
||||||
|
async def x2(self, itx: discord.Interaction, _):
|
||||||
|
if itx.user.id != self.user_id: return await itx.response.send_message("Not your panel.", ephemeral=True)
|
||||||
|
if self.in_round: return await itx.response.send_message("You can’t change bet during a round.", ephemeral=True)
|
||||||
|
self.bet = max(HILO_MIN_BET, self.bet * 2)
|
||||||
|
await itx.response.edit_message(embed=self.render(), view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label="½", style=discord.ButtonStyle.secondary, row=0)
|
||||||
|
async def half(self, itx: discord.Interaction, _):
|
||||||
|
if itx.user.id != self.user_id: return await itx.response.send_message("Not your panel.", ephemeral=True)
|
||||||
|
if self.in_round: return await itx.response.send_message("You can’t change bet during a round.", ephemeral=True)
|
||||||
|
self.bet = max(HILO_MIN_BET, self.bet // 2)
|
||||||
|
await itx.response.edit_message(embed=self.render(), view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label="Start", style=discord.ButtonStyle.success, emoji="🎴", row=0)
|
||||||
|
async def start(self, itx: discord.Interaction, _):
|
||||||
|
if itx.user.id != self.user_id: return await itx.response.send_message("Not your panel.", ephemeral=True)
|
||||||
|
if self.in_round: return await itx.response.send_message("Already in a round.", ephemeral=True)
|
||||||
|
await self._start_round(itx)
|
||||||
|
|
||||||
|
# ----- Row 1 -----
|
||||||
|
@discord.ui.button(label="LOWER", style=discord.ButtonStyle.danger, emoji="⬇️", row=1)
|
||||||
|
async def lower(self, itx: discord.Interaction, _):
|
||||||
|
if itx.user.id != self.user_id: return await itx.response.send_message("Not your panel.", ephemeral=True)
|
||||||
|
if not self.in_round or not self.current_card: return
|
||||||
|
await self._resolve_step(itx, "down")
|
||||||
|
|
||||||
|
@discord.ui.button(label="Cash Out", style=discord.ButtonStyle.secondary, emoji="💵", row=1)
|
||||||
|
async def cash(self, itx: discord.Interaction, _):
|
||||||
|
if itx.user.id != self.user_id: return await itx.response.send_message("Not your panel.", ephemeral=True)
|
||||||
|
if not self.in_round: return
|
||||||
|
await self._cashout(itx)
|
||||||
|
|
||||||
|
@discord.ui.button(label="HIGHER", style=discord.ButtonStyle.primary, emoji="⬆️", row=1)
|
||||||
|
async def higher(self, itx: discord.Interaction, _):
|
||||||
|
if itx.user.id != self.user_id: return await itx.response.send_message("Not your panel.", ephemeral=True)
|
||||||
|
if not self.in_round or not self.current_card: return
|
||||||
|
await self._resolve_step(itx, "up")
|
||||||
|
|
||||||
|
class HiloCog(commands.Cog):
|
||||||
|
def __init__(self, bot): self.bot = bot
|
||||||
|
|
||||||
|
@commands.command(name="hilo", aliases=["hi"])
|
||||||
|
async def hilo(self, ctx: commands.Context, bet: int = None):
|
||||||
|
"""Open the Hi-Lo panel."""
|
||||||
|
uid = ctx.author.id
|
||||||
|
view = HiloView(uid, initial_bet=bet or 50, timeout=180)
|
||||||
|
msg = await ctx.send(embed=view.render(), view=view)
|
||||||
|
view.message = msg
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
await bot.add_cog(HiloCog(bot))
|
||||||
450
src/cogs/mines.py
Normal file
450
src/cogs/mines.py
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
# src/cogs/mines.py
|
||||||
|
import random
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
from typing import Optional, Set, Tuple
|
||||||
|
|
||||||
|
from .. import db
|
||||||
|
|
||||||
|
# --- Config (override via constants.py if present) ---
|
||||||
|
try:
|
||||||
|
from ..utils.constants import (
|
||||||
|
MINES_MIN_BET, MINES_EDGE_PER_STEP, MINES_MAX_MULT, MINES_CHOICES
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
MINES_MIN_BET = 10
|
||||||
|
MINES_EDGE_PER_STEP = 0.015 # 1.5% house edge per safe pick
|
||||||
|
MINES_MAX_MULT = 1000.0
|
||||||
|
MINES_CHOICES = [1, 3, 5, 7] # selectable mine counts
|
||||||
|
|
||||||
|
GRID_N = 5
|
||||||
|
TOTAL_CELLS = GRID_N * GRID_N
|
||||||
|
|
||||||
|
# emojis
|
||||||
|
EMO_UNREVEALED = "⬜"
|
||||||
|
EMO_SAFE = "🟩"
|
||||||
|
EMO_MINE = "💣"
|
||||||
|
EMO_HIT = "💥" # bomb that was clicked
|
||||||
|
|
||||||
|
# labels
|
||||||
|
COL_LABELS = ["🇦","🇧","🇨","🇩","🇪"]
|
||||||
|
ROW_LABELS = ["1️⃣","2️⃣","3️⃣","4️⃣","5️⃣"]
|
||||||
|
|
||||||
|
def money(n: int) -> str:
|
||||||
|
return f"${n:,}"
|
||||||
|
|
||||||
|
def coord_to_idx(cell: Tuple[int,int]) -> int:
|
||||||
|
r, c = cell
|
||||||
|
return r * GRID_N + c
|
||||||
|
|
||||||
|
def step_multiplier(total_rem: int, safe_rem: int) -> float:
|
||||||
|
"""
|
||||||
|
Fair step MULT = 1 / P(safe) = total_rem / safe_rem.
|
||||||
|
Apply per-step house edge, clamp to >= 1.0 and <= cap.
|
||||||
|
"""
|
||||||
|
if safe_rem <= 0 or total_rem <= 0:
|
||||||
|
return 1.0
|
||||||
|
fair = total_rem / safe_rem
|
||||||
|
mult = fair * (1.0 - MINES_EDGE_PER_STEP)
|
||||||
|
return max(1.0, min(mult, MINES_MAX_MULT))
|
||||||
|
|
||||||
|
class SetBetModal(discord.ui.Modal, title="Set Mines Bet"):
|
||||||
|
def __init__(self, view: "MinesView"):
|
||||||
|
super().__init__()
|
||||||
|
self.view = view
|
||||||
|
self.amount = discord.ui.TextInput(
|
||||||
|
label=f"Amount (min {MINES_MIN_BET})",
|
||||||
|
placeholder=str(self.view.bet),
|
||||||
|
required=True, min_length=1, max_length=10,
|
||||||
|
)
|
||||||
|
self.add_item(self.amount)
|
||||||
|
|
||||||
|
async def on_submit(self, itx: discord.Interaction):
|
||||||
|
if itx.user.id != self.view.user_id:
|
||||||
|
return await itx.response.send_message("This isn’t your Mines panel.", ephemeral=True)
|
||||||
|
if self.view.in_round:
|
||||||
|
return await itx.response.send_message("You can’t change bet during a round.", ephemeral=True)
|
||||||
|
raw = str(self.amount.value)
|
||||||
|
try:
|
||||||
|
amt = int("".join(ch for ch in raw if ch.isdigit()))
|
||||||
|
except Exception:
|
||||||
|
return await itx.response.send_message("Enter a valid number.", ephemeral=True)
|
||||||
|
self.view.bet = max(MINES_MIN_BET, amt)
|
||||||
|
await itx.response.defer(ephemeral=True)
|
||||||
|
if self.view.message:
|
||||||
|
await self.view.message.edit(embed=self.view.render(), view=self.view)
|
||||||
|
|
||||||
|
class PickCellModal(discord.ui.Modal, title="Pick a Cell (A1–E5)"):
|
||||||
|
def __init__(self, view: "MinesView"):
|
||||||
|
super().__init__()
|
||||||
|
self.view = view
|
||||||
|
self.cell = discord.ui.TextInput(
|
||||||
|
label="Cell (e.g., A1, C3, E5)",
|
||||||
|
placeholder="A1",
|
||||||
|
required=True,
|
||||||
|
min_length=2, max_length=3,
|
||||||
|
)
|
||||||
|
self.add_item(self.cell)
|
||||||
|
|
||||||
|
async def on_submit(self, itx: discord.Interaction):
|
||||||
|
if itx.user.id != self.view.user_id:
|
||||||
|
return await itx.response.send_message("This isn’t your Mines panel.", ephemeral=True)
|
||||||
|
if not self.view.in_round:
|
||||||
|
return await itx.response.send_message("No active round.", ephemeral=True)
|
||||||
|
raw = self.cell.value.strip().upper().replace(" ", "")
|
||||||
|
if len(raw) < 2:
|
||||||
|
return await itx.response.send_message("Invalid cell.", ephemeral=True)
|
||||||
|
col_ch = raw[0]
|
||||||
|
row_str = raw[1:]
|
||||||
|
if col_ch not in "ABCDE":
|
||||||
|
return await itx.response.send_message("Column must be A–E.", ephemeral=True)
|
||||||
|
if not row_str.isdigit():
|
||||||
|
return await itx.response.send_message("Row must be 1–5.", ephemeral=True)
|
||||||
|
r = int(row_str) - 1
|
||||||
|
c = "ABCDE".index(col_ch)
|
||||||
|
if r < 0 or r >= GRID_N or c < 0 or c >= GRID_N:
|
||||||
|
return await itx.response.send_message("Out of range.", ephemeral=True)
|
||||||
|
await itx.response.defer(ephemeral=True)
|
||||||
|
await self.view.reveal_cell((r, c))
|
||||||
|
|
||||||
|
class MinesView(discord.ui.View):
|
||||||
|
"""
|
||||||
|
Row 0: Set Bet · ×2 · ½ · Mines:[N] · Start
|
||||||
|
Row 1: Pick Cell · Pick Random · Cash Out
|
||||||
|
"""
|
||||||
|
def __init__(self, user_id: int, initial_bet: int = 50, *, timeout: int = 180):
|
||||||
|
super().__init__(timeout=timeout)
|
||||||
|
self.user_id = user_id
|
||||||
|
self.bet = max(MINES_MIN_BET, initial_bet)
|
||||||
|
|
||||||
|
self.mines_options = MINES_CHOICES[:]
|
||||||
|
self.mines_index = 0
|
||||||
|
self.mines_count = self.mines_options[self.mines_index]
|
||||||
|
|
||||||
|
# round state
|
||||||
|
self.in_round = False
|
||||||
|
self.mines: Set[int] = set()
|
||||||
|
self.revealed: Set[int] = set()
|
||||||
|
self.mult = 1.0
|
||||||
|
self.total_rem = TOTAL_CELLS
|
||||||
|
self.safe_rem = TOTAL_CELLS - self.mines_count
|
||||||
|
|
||||||
|
self.last_summary: Optional[dict] = None # show after round ends
|
||||||
|
self.message: Optional[discord.Message] = None
|
||||||
|
self._busy = False
|
||||||
|
|
||||||
|
self._sync_buttons()
|
||||||
|
|
||||||
|
# ----- lifecycle -----
|
||||||
|
def disable_all(self):
|
||||||
|
for c in self.children:
|
||||||
|
c.disabled = True
|
||||||
|
|
||||||
|
async def on_timeout(self):
|
||||||
|
self.disable_all()
|
||||||
|
try:
|
||||||
|
if self.message:
|
||||||
|
await self.message.edit(view=self)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ----- rendering -----
|
||||||
|
def _grid_text(
|
||||||
|
self,
|
||||||
|
show_mines: bool = False,
|
||||||
|
with_labels: bool = True,
|
||||||
|
*,
|
||||||
|
mines: Optional[Set[int]] = None,
|
||||||
|
revealed: Optional[Set[int]] = None,
|
||||||
|
hit_idx: Optional[int] = None
|
||||||
|
) -> str:
|
||||||
|
"""Return a labeled 5×5 grid as lines of emojis. Can render from snapshots."""
|
||||||
|
mines = self.mines if mines is None else mines
|
||||||
|
revealed = self.revealed if revealed is None else revealed
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
if with_labels:
|
||||||
|
lines.append("⬛ " + " ".join(COL_LABELS))
|
||||||
|
for r in range(GRID_N):
|
||||||
|
row_emojis = []
|
||||||
|
for c in range(GRID_N):
|
||||||
|
idx = coord_to_idx((r, c))
|
||||||
|
if idx in revealed:
|
||||||
|
# if we want the clicked bomb to show as 💥 even if counted as revealed, prefer 💥
|
||||||
|
if show_mines and hit_idx is not None and idx == hit_idx:
|
||||||
|
row_emojis.append(EMO_HIT)
|
||||||
|
else:
|
||||||
|
row_emojis.append(EMO_SAFE)
|
||||||
|
else:
|
||||||
|
if show_mines and idx in mines:
|
||||||
|
row_emojis.append(EMO_HIT if (hit_idx is not None and idx == hit_idx) else EMO_MINE)
|
||||||
|
else:
|
||||||
|
row_emojis.append(EMO_UNREVEALED)
|
||||||
|
line = " ".join(row_emojis)
|
||||||
|
if with_labels:
|
||||||
|
line = f"{ROW_LABELS[r]} {line}"
|
||||||
|
lines.append(line)
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def render(self) -> discord.Embed:
|
||||||
|
# color based on last result or active
|
||||||
|
if self.in_round:
|
||||||
|
color = discord.Color.green()
|
||||||
|
elif self.last_summary is not None:
|
||||||
|
net = self.last_summary.get("net", 0)
|
||||||
|
color = discord.Color.green() if net > 0 else (discord.Color.red() if net < 0 else discord.Color.orange())
|
||||||
|
else:
|
||||||
|
color = discord.Color.blurple()
|
||||||
|
|
||||||
|
e = discord.Embed(title="💣 Mines", color=color)
|
||||||
|
cash, _ = db.get_wallet(self.user_id)
|
||||||
|
|
||||||
|
if not self.in_round:
|
||||||
|
desc = [
|
||||||
|
f"**Bet:** {money(self.bet)} • **Mines:** {self.mines_count}",
|
||||||
|
f"**Balance:** {money(cash)}",
|
||||||
|
"",
|
||||||
|
"Press **Start** to place mines and begin. Then **Pick Cell** (A1–E5) or **Pick Random**.",
|
||||||
|
"Each safe pick increases your multiplier; **Cash Out** anytime.",
|
||||||
|
]
|
||||||
|
if self.last_summary is not None:
|
||||||
|
sym = "💰" if self.last_summary["net"] > 0 else ("❌" if self.last_summary["net"] < 0 else "↔️")
|
||||||
|
returned = self.last_summary["returned"]
|
||||||
|
net = self.last_summary["net"]
|
||||||
|
picks = self.last_summary["picks"]
|
||||||
|
mult = self.last_summary["mult"]
|
||||||
|
how = self.last_summary["how"]
|
||||||
|
if how == "MINE":
|
||||||
|
where = self.last_summary.get("cell_str", "?")
|
||||||
|
desc += [
|
||||||
|
"",
|
||||||
|
f"**Last Result:** {sym} Hit a mine at **{where}**.",
|
||||||
|
f"Safe picks: **{picks}**, Mult: **×{mult:.2f}**, Returned: **{money(returned)}**, Net: **{'+' if net>0 else ''}{money(net)}**",
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
desc += [
|
||||||
|
"",
|
||||||
|
f"**Last Result:** {sym} Cashed out.",
|
||||||
|
f"Safe picks: **{picks}**, Mult: **×{mult:.2f}**, Returned: **{money(returned)}**, Net: **{'+' if net>0 else ''}{money(net)}**",
|
||||||
|
]
|
||||||
|
e.description = "\n".join(desc)
|
||||||
|
|
||||||
|
# NEW: show snapshot board if present
|
||||||
|
if self.last_summary is not None and self.last_summary.get("board"):
|
||||||
|
e.add_field(name="Board", value=self.last_summary["board"], inline=False)
|
||||||
|
|
||||||
|
return e
|
||||||
|
|
||||||
|
# in-round panel
|
||||||
|
grid = self._grid_text(show_mines=False, with_labels=True)
|
||||||
|
e.description = (
|
||||||
|
f"**Bet:** {money(self.bet)} • **Mines:** {self.mines_count}\n"
|
||||||
|
f"**Picks:** {len(self.revealed)} • **Multiplier:** ×{self.mult:.2f} • "
|
||||||
|
f"**Cashout:** {money(int(self.bet * self.mult))}\n\n{grid}"
|
||||||
|
)
|
||||||
|
e.set_footer(text="Use Pick Cell (A1–E5) or Pick Random. Cash Out anytime.")
|
||||||
|
return e
|
||||||
|
|
||||||
|
# ----- helpers -----
|
||||||
|
def _sync_buttons(self):
|
||||||
|
active = self.in_round
|
||||||
|
# Row 0 controls: disabled in-round (except Start which is disabled once in-round)
|
||||||
|
for lbl in ("Set Bet", "×2", "½", None, "Start"):
|
||||||
|
b = self._btn(lbl) if lbl else None
|
||||||
|
if b:
|
||||||
|
b.disabled = active if lbl != "Start" else active
|
||||||
|
# Mines button enabled only when not in round
|
||||||
|
mines_btn = self._btn(f"Mines: {self.mines_count}")
|
||||||
|
if mines_btn:
|
||||||
|
mines_btn.disabled = active
|
||||||
|
|
||||||
|
# Row 1 controls: enabled only in-round
|
||||||
|
for lbl in ("Pick Cell", "Pick Random", "Cash Out"):
|
||||||
|
b = self._btn(lbl)
|
||||||
|
if b: b.disabled = not active
|
||||||
|
|
||||||
|
def _btn(self, label: Optional[str]):
|
||||||
|
for c in self.children:
|
||||||
|
if isinstance(c, discord.ui.Button) and c.label == label:
|
||||||
|
return c
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _reset_round(self):
|
||||||
|
self.in_round = False
|
||||||
|
self.mines.clear()
|
||||||
|
self.revealed.clear()
|
||||||
|
self.mult = 1.0
|
||||||
|
self.total_rem = TOTAL_CELLS
|
||||||
|
self.safe_rem = TOTAL_CELLS - self.mines_count
|
||||||
|
self._sync_buttons()
|
||||||
|
|
||||||
|
async def start_round(self, itx: discord.Interaction):
|
||||||
|
cash, _ = db.get_wallet(self.user_id)
|
||||||
|
if self.bet > cash:
|
||||||
|
return await itx.response.send_message("Not enough cash for that bet.", ephemeral=True)
|
||||||
|
|
||||||
|
# debit and place mines
|
||||||
|
db.add_cash(self.user_id, -self.bet)
|
||||||
|
self.in_round = True
|
||||||
|
self.mult = 1.0
|
||||||
|
self.revealed = set()
|
||||||
|
all_idx = list(range(TOTAL_CELLS))
|
||||||
|
self.mines = set(random.sample(all_idx, self.mines_count))
|
||||||
|
self.total_rem = TOTAL_CELLS
|
||||||
|
self.safe_rem = TOTAL_CELLS - self.mines_count
|
||||||
|
self._sync_buttons()
|
||||||
|
await itx.response.edit_message(embed=self.render(), view=self)
|
||||||
|
|
||||||
|
def _cell_str(self, rc: Tuple[int,int]) -> str:
|
||||||
|
col = "ABCDE"[rc[1]]
|
||||||
|
row = rc[0] + 1
|
||||||
|
return f"{col}{row}"
|
||||||
|
|
||||||
|
async def reveal_cell(self, rc: Tuple[int,int]):
|
||||||
|
if self._busy or not self.in_round:
|
||||||
|
return
|
||||||
|
self._busy = True
|
||||||
|
idx = coord_to_idx(rc)
|
||||||
|
|
||||||
|
# already revealed?
|
||||||
|
if idx in self.revealed:
|
||||||
|
self._busy = False
|
||||||
|
return
|
||||||
|
|
||||||
|
if idx in self.mines:
|
||||||
|
# build snapshot BEFORE resetting: show all mines and mark the hit
|
||||||
|
snapshot_board = self._grid_text(
|
||||||
|
show_mines=True, with_labels=True,
|
||||||
|
mines=set(self.mines), revealed=set(self.revealed), hit_idx=idx
|
||||||
|
)
|
||||||
|
|
||||||
|
returned = 0
|
||||||
|
db.record_mines(self.user_id, bet=self.bet, return_amount=returned, won=False)
|
||||||
|
self.last_summary = {
|
||||||
|
"how": "MINE",
|
||||||
|
"cell_str": self._cell_str(rc),
|
||||||
|
"picks": len(self.revealed),
|
||||||
|
"mult": self.mult,
|
||||||
|
"returned": returned,
|
||||||
|
"net": -self.bet,
|
||||||
|
"board": snapshot_board,
|
||||||
|
}
|
||||||
|
self._reset_round()
|
||||||
|
if self.message:
|
||||||
|
await self.message.edit(embed=self.render(), view=self)
|
||||||
|
self._busy = False
|
||||||
|
return
|
||||||
|
|
||||||
|
# success — update multiplier and state
|
||||||
|
step_mult = step_multiplier(self.total_rem, self.safe_rem)
|
||||||
|
self.mult = min(self.mult * step_mult, MINES_MAX_MULT)
|
||||||
|
self.revealed.add(idx)
|
||||||
|
self.total_rem -= 1
|
||||||
|
self.safe_rem -= 1
|
||||||
|
|
||||||
|
if self.message:
|
||||||
|
await self.message.edit(embed=self.render(), view=self)
|
||||||
|
self._busy = False
|
||||||
|
|
||||||
|
async def pick_random(self, itx: discord.Interaction):
|
||||||
|
# choose any unrevealed cell uniformly
|
||||||
|
unrevealed = [i for i in range(TOTAL_CELLS) if i not in self.revealed]
|
||||||
|
if not unrevealed:
|
||||||
|
return await itx.response.send_message("All cells are revealed!", ephemeral=True)
|
||||||
|
choice = random.choice(unrevealed)
|
||||||
|
r, c = divmod(choice, GRID_N)
|
||||||
|
await itx.response.defer(ephemeral=True)
|
||||||
|
await self.reveal_cell((r, c))
|
||||||
|
|
||||||
|
async def cash_out(self, itx: discord.Interaction):
|
||||||
|
if not self.in_round:
|
||||||
|
return await itx.response.send_message("No active round.", ephemeral=True)
|
||||||
|
returned = int(self.bet * self.mult)
|
||||||
|
if returned > 0:
|
||||||
|
db.add_cash(self.user_id, returned)
|
||||||
|
db.record_mines(self.user_id, bet=self.bet, return_amount=returned, won=(returned > self.bet))
|
||||||
|
|
||||||
|
# snapshot of the revealed greens at cashout
|
||||||
|
snapshot_board = self._grid_text(
|
||||||
|
show_mines=False, with_labels=True,
|
||||||
|
mines=set(self.mines), revealed=set(self.revealed)
|
||||||
|
)
|
||||||
|
self.last_summary = {
|
||||||
|
"how": "CASH",
|
||||||
|
"picks": len(self.revealed),
|
||||||
|
"mult": self.mult,
|
||||||
|
"returned": returned,
|
||||||
|
"net": returned - self.bet,
|
||||||
|
"board": snapshot_board,
|
||||||
|
}
|
||||||
|
self._reset_round()
|
||||||
|
await itx.response.edit_message(embed=self.render(), view=self)
|
||||||
|
|
||||||
|
# ----- Row 0 controls -----
|
||||||
|
@discord.ui.button(label="Set Bet", style=discord.ButtonStyle.secondary, row=0)
|
||||||
|
async def set_bet(self, itx: discord.Interaction, _):
|
||||||
|
if itx.user.id != self.user_id: return await itx.response.send_message("Not your panel.", ephemeral=True)
|
||||||
|
if self.in_round: return await itx.response.send_message("You can’t change bet during a round.", ephemeral=True)
|
||||||
|
await itx.response.send_modal(SetBetModal(self))
|
||||||
|
|
||||||
|
@discord.ui.button(label="×2", style=discord.ButtonStyle.secondary, row=0)
|
||||||
|
async def x2(self, itx: discord.Interaction, _):
|
||||||
|
if itx.user.id != self.user_id: return await itx.response.send_message("Not your panel.", ephemeral=True)
|
||||||
|
if self.in_round: return await itx.response.send_message("You can’t change bet during a round.", ephemeral=True)
|
||||||
|
self.bet = max(MINES_MIN_BET, self.bet * 2)
|
||||||
|
await itx.response.edit_message(embed=self.render(), view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label="½", style=discord.ButtonStyle.secondary, row=0)
|
||||||
|
async def half(self, itx: discord.Interaction, _):
|
||||||
|
if itx.user.id != self.user_id: return await itx.response.send_message("Not your panel.", ephemeral=True)
|
||||||
|
if self.in_round: return await itx.response.send_message("You can’t change bet during a round.", ephemeral=True)
|
||||||
|
self.bet = max(MINES_MIN_BET, self.bet // 2)
|
||||||
|
await itx.response.edit_message(embed=self.render(), view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label="Start", style=discord.ButtonStyle.success, emoji="🚩", row=0)
|
||||||
|
async def start(self, itx: discord.Interaction, _):
|
||||||
|
if itx.user.id != self.user_id: return await itx.response.send_message("Not your panel.", ephemeral=True)
|
||||||
|
if self.in_round: return await itx.response.send_message("Already in a round.", ephemeral=True)
|
||||||
|
await self.start_round(itx)
|
||||||
|
|
||||||
|
# "Mines: N" (cycles through your MINES_CHOICES)
|
||||||
|
@discord.ui.button(label="Mines: 1", style=discord.ButtonStyle.secondary, row=0)
|
||||||
|
async def mines_btn(self, itx: discord.Interaction, button: discord.ui.Button):
|
||||||
|
if itx.user.id != self.user_id: return await itx.response.send_message("Not your panel.", ephemeral=True)
|
||||||
|
if self.in_round: return await itx.response.send_message("Change mines before starting.", ephemeral=True)
|
||||||
|
self.mines_index = (self.mines_index + 1) % len(self.mines_options)
|
||||||
|
self.mines_count = self.mines_options[self.mines_index]
|
||||||
|
button.label = f"Mines: {self.mines_count}"
|
||||||
|
await itx.response.edit_message(embed=self.render(), view=self)
|
||||||
|
|
||||||
|
# ----- Row 1 controls -----
|
||||||
|
@discord.ui.button(label="Pick Cell", style=discord.ButtonStyle.primary, emoji="🎯", row=1)
|
||||||
|
async def pick_cell(self, itx: discord.Interaction, _):
|
||||||
|
if itx.user.id != self.user_id: return await itx.response.send_message("Not your panel.", ephemeral=True)
|
||||||
|
if not self.in_round: return await itx.response.send_message("No active round.", ephemeral=True)
|
||||||
|
await itx.response.send_modal(PickCellModal(self))
|
||||||
|
|
||||||
|
@discord.ui.button(label="Pick Random", style=discord.ButtonStyle.secondary, emoji="🎲", row=1)
|
||||||
|
async def pick_rand(self, itx: discord.Interaction, _):
|
||||||
|
if itx.user.id != self.user_id: return await itx.response.send_message("Not your panel.", ephemeral=True)
|
||||||
|
if not self.in_round: return await itx.response.send_message("No active round.", ephemeral=True)
|
||||||
|
await self.pick_random(itx)
|
||||||
|
|
||||||
|
@discord.ui.button(label="Cash Out", style=discord.ButtonStyle.danger, emoji="💵", row=1)
|
||||||
|
async def cash(self, itx: discord.Interaction, _):
|
||||||
|
if itx.user.id != self.user_id: return await itx.response.send_message("Not your panel.", ephemeral=True)
|
||||||
|
if not self.in_round: return await itx.response.send_message("No active round.", ephemeral=True)
|
||||||
|
await self.cash_out(itx)
|
||||||
|
|
||||||
|
class MinesCog(commands.Cog):
|
||||||
|
def __init__(self, bot): self.bot = bot
|
||||||
|
|
||||||
|
@commands.command(name="mines")
|
||||||
|
async def mines(self, ctx: commands.Context, bet: int = None):
|
||||||
|
uid = ctx.author.id
|
||||||
|
view = MinesView(uid, initial_bet=bet or MINES_MIN_BET, timeout=180)
|
||||||
|
msg = await ctx.send(embed=view.render(), view=view)
|
||||||
|
view.message = msg
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
await bot.add_cog(MinesCog(bot))
|
||||||
469
src/cogs/packs.py
Normal file
469
src/cogs/packs.py
Normal file
@@ -0,0 +1,469 @@
|
|||||||
|
# src/cogs/packs.py
|
||||||
|
import random
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
from typing import Optional, Dict, List, Tuple
|
||||||
|
|
||||||
|
from .. import db
|
||||||
|
|
||||||
|
# ---- Tunables (override via constants.py if you want) ----
|
||||||
|
try:
|
||||||
|
from ..utils.constants import PACK_MIN_BET, PACK_SIZE, PACK_RARITY_WEIGHTS
|
||||||
|
except Exception:
|
||||||
|
PACK_MIN_BET = 50
|
||||||
|
PACK_SIZE = 5
|
||||||
|
# weights per rarity (sum doesn't need to be 1; we normalize)
|
||||||
|
PACK_RARITY_WEIGHTS = {
|
||||||
|
"Common": 600,
|
||||||
|
"Uncommon": 250,
|
||||||
|
"Rare": 100,
|
||||||
|
"Epic": 40,
|
||||||
|
"Legendary": 9,
|
||||||
|
"Mythic": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Catalog: id, emoji, display name, rarity, multiplier (× bet)
|
||||||
|
# Kept your original 25 items & IDs; expanded to 100 total.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
CATALOG: List[Dict] = [
|
||||||
|
# ---------------------- Common (40) ----------------------
|
||||||
|
{"id":"cherry","emoji":"🍒","name":"Cherries","rarity":"Common","mult":0.10},
|
||||||
|
{"id":"coin","emoji":"🪙","name":"Coin","rarity":"Common","mult":0.12},
|
||||||
|
{"id":"mug","emoji":"☕","name":"Mug","rarity":"Common","mult":0.15},
|
||||||
|
{"id":"leaf","emoji":"🌿","name":"Leaf","rarity":"Common","mult":0.18},
|
||||||
|
{"id":"sock","emoji":"🧦","name":"Sock","rarity":"Common","mult":0.20},
|
||||||
|
{"id":"teddy","emoji":"🧸","name":"Teddy","rarity":"Common","mult":0.22},
|
||||||
|
{"id":"ice","emoji":"🧊","name":"Ice","rarity":"Common","mult":0.24},
|
||||||
|
{"id":"clover","emoji":"🍀","name":"Clover","rarity":"Common","mult":0.25},
|
||||||
|
|
||||||
|
{"id":"apple","emoji":"🍎","name":"Apple","rarity":"Common","mult":0.12},
|
||||||
|
{"id":"banana","emoji":"🍌","name":"Banana","rarity":"Common","mult":0.14},
|
||||||
|
{"id":"pear","emoji":"🍐","name":"Pear","rarity":"Common","mult":0.13},
|
||||||
|
{"id":"peach","emoji":"🍑","name":"Peach","rarity":"Common","mult":0.16},
|
||||||
|
{"id":"strawberry","emoji":"🍓","name":"Strawberry","rarity":"Common","mult":0.18},
|
||||||
|
{"id":"grapes","emoji":"🍇","name":"Grapes","rarity":"Common","mult":0.20},
|
||||||
|
{"id":"carrot","emoji":"🥕","name":"Carrot","rarity":"Common","mult":0.12},
|
||||||
|
{"id":"corn","emoji":"🌽","name":"Corn","rarity":"Common","mult":0.15},
|
||||||
|
{"id":"bread","emoji":"🍞","name":"Bread","rarity":"Common","mult":0.14},
|
||||||
|
{"id":"cheese","emoji":"🧀","name":"Cheese","rarity":"Common","mult":0.18},
|
||||||
|
{"id":"egg","emoji":"🥚","name":"Egg","rarity":"Common","mult":0.16},
|
||||||
|
{"id":"milk","emoji":"🥛","name":"Milk","rarity":"Common","mult":0.12},
|
||||||
|
{"id":"mushroom","emoji":"🍄","name":"Mushroom","rarity":"Common","mult":0.20},
|
||||||
|
{"id":"seedling","emoji":"🌱","name":"Seedling","rarity":"Common","mult":0.16},
|
||||||
|
{"id":"flower","emoji":"🌼","name":"Flower","rarity":"Common","mult":0.16},
|
||||||
|
{"id":"maple","emoji":"🍁","name":"Maple Leaf","rarity":"Common","mult":0.18},
|
||||||
|
{"id":"sun","emoji":"☀️","name":"Sun","rarity":"Common","mult":0.24},
|
||||||
|
{"id":"moon","emoji":"🌙","name":"Moon","rarity":"Common","mult":0.22},
|
||||||
|
{"id":"cloud","emoji":"☁️","name":"Cloud","rarity":"Common","mult":0.18},
|
||||||
|
{"id":"snowflake","emoji":"❄️","name":"Snowflake","rarity":"Common","mult":0.20},
|
||||||
|
{"id":"umbrella","emoji":"☂️","name":"Umbrella","rarity":"Common","mult":0.18},
|
||||||
|
{"id":"balloon","emoji":"🎈","name":"Balloon","rarity":"Common","mult":0.22},
|
||||||
|
{"id":"pencil","emoji":"✏️","name":"Pencil","rarity":"Common","mult":0.14},
|
||||||
|
{"id":"book","emoji":"📘","name":"Book","rarity":"Common","mult":0.16},
|
||||||
|
{"id":"paperclip","emoji":"🖇️","name":"Paperclip","rarity":"Common","mult":0.14},
|
||||||
|
{"id":"scissors","emoji":"✂️","name":"Scissors","rarity":"Common","mult":0.16},
|
||||||
|
{"id":"bulb","emoji":"💡","name":"Light Bulb","rarity":"Common","mult":0.24},
|
||||||
|
{"id":"battery","emoji":"🔋","name":"Battery","rarity":"Common","mult":0.22},
|
||||||
|
{"id":"wrench","emoji":"🔧","name":"Wrench","rarity":"Common","mult":0.20},
|
||||||
|
{"id":"hammer","emoji":"🔨","name":"Hammer","rarity":"Common","mult":0.20},
|
||||||
|
{"id":"camera","emoji":"📷","name":"Camera","rarity":"Common","mult":0.24},
|
||||||
|
{"id":"gamepad","emoji":"🎮","name":"Gamepad","rarity":"Common","mult":0.24},
|
||||||
|
|
||||||
|
# -------------------- Uncommon (25) ---------------------
|
||||||
|
{"id":"donut","emoji":"🍩","name":"Donut","rarity":"Uncommon","mult":0.35},
|
||||||
|
{"id":"pizza","emoji":"🍕","name":"Pizza","rarity":"Uncommon","mult":0.40},
|
||||||
|
{"id":"soccer","emoji":"⚽","name":"Soccer Ball","rarity":"Uncommon","mult":0.45},
|
||||||
|
{"id":"headset","emoji":"🎧","name":"Headset","rarity":"Uncommon","mult":0.50},
|
||||||
|
{"id":"magnet","emoji":"🧲","name":"Magnet","rarity":"Uncommon","mult":0.55},
|
||||||
|
{"id":"cat","emoji":"🐱","name":"Cat","rarity":"Uncommon","mult":0.60},
|
||||||
|
|
||||||
|
{"id":"basketball","emoji":"🏀","name":"Basketball","rarity":"Uncommon","mult":0.45},
|
||||||
|
{"id":"baseball","emoji":"⚾","name":"Baseball","rarity":"Uncommon","mult":0.42},
|
||||||
|
{"id":"guitar","emoji":"🎸","name":"Guitar","rarity":"Uncommon","mult":0.55},
|
||||||
|
{"id":"violin","emoji":"🎻","name":"Violin","rarity":"Uncommon","mult":0.58},
|
||||||
|
{"id":"joystick","emoji":"🕹️","name":"Joystick","rarity":"Uncommon","mult":0.50},
|
||||||
|
{"id":"keyboard","emoji":"⌨️","name":"Keyboard","rarity":"Uncommon","mult":0.48},
|
||||||
|
{"id":"laptop","emoji":"💻","name":"Laptop","rarity":"Uncommon","mult":0.60},
|
||||||
|
{"id":"robot","emoji":"🤖","name":"Robot","rarity":"Uncommon","mult":0.62},
|
||||||
|
{"id":"dog","emoji":"🐶","name":"Dog","rarity":"Uncommon","mult":0.55},
|
||||||
|
{"id":"fox","emoji":"🦊","name":"Fox","rarity":"Uncommon","mult":0.58},
|
||||||
|
{"id":"penguin","emoji":"🐧","name":"Penguin","rarity":"Uncommon","mult":0.60},
|
||||||
|
{"id":"koala","emoji":"🐨","name":"Koala","rarity":"Uncommon","mult":0.60},
|
||||||
|
{"id":"panda","emoji":"🐼","name":"Panda","rarity":"Uncommon","mult":0.60},
|
||||||
|
{"id":"owl","emoji":"🦉","name":"Owl","rarity":"Uncommon","mult":0.65},
|
||||||
|
{"id":"butterfly","emoji":"🦋","name":"Butterfly","rarity":"Uncommon","mult":0.65},
|
||||||
|
{"id":"car","emoji":"🚗","name":"Car","rarity":"Uncommon","mult":0.55},
|
||||||
|
{"id":"train","emoji":"🚆","name":"Train","rarity":"Uncommon","mult":0.55},
|
||||||
|
{"id":"sailboat","emoji":"⛵","name":"Sailboat","rarity":"Uncommon","mult":0.58},
|
||||||
|
{"id":"airplane","emoji":"✈️","name":"Airplane","rarity":"Uncommon","mult":0.62},
|
||||||
|
|
||||||
|
# ---------------------- Rare (18) -----------------------
|
||||||
|
{"id":"melon","emoji":"🍉","name":"Watermelon","rarity":"Rare","mult":0.85},
|
||||||
|
{"id":"tiger","emoji":"🐯","name":"Tiger","rarity":"Rare","mult":1.00},
|
||||||
|
{"id":"ufo","emoji":"🛸","name":"UFO","rarity":"Rare","mult":1.10},
|
||||||
|
{"id":"unicorn","emoji":"🦄","name":"Unicorn","rarity":"Rare","mult":1.20},
|
||||||
|
|
||||||
|
{"id":"elephant","emoji":"🐘","name":"Elephant","rarity":"Rare","mult":0.95},
|
||||||
|
{"id":"lion","emoji":"🦁","name":"Lion","rarity":"Rare","mult":1.05},
|
||||||
|
{"id":"wolf","emoji":"🐺","name":"Wolf","rarity":"Rare","mult":1.05},
|
||||||
|
{"id":"dolphin","emoji":"🐬","name":"Dolphin","rarity":"Rare","mult":0.90},
|
||||||
|
{"id":"whale","emoji":"🐳","name":"Whale","rarity":"Rare","mult":0.90},
|
||||||
|
{"id":"alien","emoji":"👽","name":"Alien","rarity":"Rare","mult":1.10},
|
||||||
|
{"id":"ghost","emoji":"👻","name":"Ghost","rarity":"Rare","mult":1.00},
|
||||||
|
{"id":"crystalball","emoji":"🔮","name":"Crystal Ball","rarity":"Rare","mult":1.10},
|
||||||
|
{"id":"satellite","emoji":"🛰️","name":"Satellite","rarity":"Rare","mult":1.15},
|
||||||
|
{"id":"comet","emoji":"☄️","name":"Comet","rarity":"Rare","mult":1.20},
|
||||||
|
{"id":"ninja","emoji":"🥷","name":"Ninja","rarity":"Rare","mult":1.10},
|
||||||
|
{"id":"mountain","emoji":"⛰️","name":"Mountain","rarity":"Rare","mult":0.95},
|
||||||
|
{"id":"volcano","emoji":"🌋","name":"Volcano","rarity":"Rare","mult":1.00},
|
||||||
|
{"id":"ring_rare","emoji":"💍","name":"Ring","rarity":"Rare","mult":1.25},
|
||||||
|
|
||||||
|
# ---------------------- Epic (10) -----------------------
|
||||||
|
{"id":"dragon","emoji":"🐉","name":"Dragon","rarity":"Epic","mult":2.0},
|
||||||
|
{"id":"lab","emoji":"🧪","name":"Lab Flask","rarity":"Epic","mult":2.5},
|
||||||
|
{"id":"rocket","emoji":"🚀","name":"Rocket","rarity":"Epic","mult":3.0},
|
||||||
|
|
||||||
|
{"id":"wizard","emoji":"🧙","name":"Wizard","rarity":"Epic","mult":2.2},
|
||||||
|
{"id":"wand","emoji":"🪄","name":"Magic Wand","rarity":"Epic","mult":2.6},
|
||||||
|
{"id":"dragonface","emoji":"🐲","name":"Dragon Face","rarity":"Epic","mult":2.8},
|
||||||
|
{"id":"castle","emoji":"🏰","name":"Castle","rarity":"Epic","mult":2.4},
|
||||||
|
{"id":"shield","emoji":"🛡️","name":"Shield","rarity":"Epic","mult":2.3},
|
||||||
|
{"id":"swords","emoji":"⚔️","name":"Crossed Swords","rarity":"Epic","mult":2.7},
|
||||||
|
{"id":"trophy_epic","emoji":"🏆","name":"Trophy","rarity":"Epic","mult":3.2},
|
||||||
|
|
||||||
|
# ------------------- Legendary (5) ----------------------
|
||||||
|
{"id":"crown","emoji":"👑","name":"Crown","rarity":"Legendary","mult":6.0},
|
||||||
|
{"id":"eagle","emoji":"🦅","name":"Eagle","rarity":"Legendary","mult":7.5},
|
||||||
|
|
||||||
|
{"id":"medal","emoji":"🥇","name":"Gold Medal","rarity":"Legendary","mult":9.0},
|
||||||
|
{"id":"moneybag","emoji":"💰","name":"Money Bag","rarity":"Legendary","mult":8.5},
|
||||||
|
{"id":"ring_legend","emoji":"💍","name":"Royal Ring","rarity":"Legendary","mult":10.0},
|
||||||
|
|
||||||
|
# --------------------- Mythic (2) -----------------------
|
||||||
|
{"id":"dino","emoji":"🦖","name":"Dino","rarity":"Mythic","mult":25.0},
|
||||||
|
{"id":"diamond","emoji":"💎","name":"Diamond","rarity":"Mythic","mult":50.0},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Build rarity index
|
||||||
|
RARITY_TO_ITEMS: Dict[str, List[Dict]] = {}
|
||||||
|
for it in CATALOG:
|
||||||
|
RARITY_TO_ITEMS.setdefault(it["rarity"], []).append(it)
|
||||||
|
|
||||||
|
def _norm_weights(weights: Dict[str, float]) -> Dict[str, float]:
|
||||||
|
s = float(sum(weights.values()))
|
||||||
|
return {k: (v / s if s > 0 else 0.0) for k, v in weights.items()}
|
||||||
|
|
||||||
|
NORM_RARITY = _norm_weights(PACK_RARITY_WEIGHTS)
|
||||||
|
|
||||||
|
def _weighted_choice(items: List[Tuple[float, Dict]]) -> Dict:
|
||||||
|
"""items: list of (weight, item) where weight >= 0"""
|
||||||
|
total = sum(w for w, _ in items)
|
||||||
|
r = random.random() * total
|
||||||
|
upto = 0.0
|
||||||
|
for w, it in items:
|
||||||
|
upto += w
|
||||||
|
if r <= upto:
|
||||||
|
return it
|
||||||
|
return items[-1][1]
|
||||||
|
|
||||||
|
def pick_one() -> Dict:
|
||||||
|
# pick rarity
|
||||||
|
rar_roll = _weighted_choice([(p, {"rarity": r}) for r, p in NORM_RARITY.items()])["rarity"]
|
||||||
|
pool = RARITY_TO_ITEMS.get(rar_roll, CATALOG)
|
||||||
|
# equal within rarity
|
||||||
|
return random.choice(pool)
|
||||||
|
|
||||||
|
def money(n: int) -> str:
|
||||||
|
return f"${n:,}"
|
||||||
|
|
||||||
|
# ---------- Pagination helpers ----------
|
||||||
|
RARITY_RANK = {"Mythic": 0, "Legendary": 1, "Epic": 2, "Rare": 3, "Uncommon": 4, "Common": 5}
|
||||||
|
SORTED_CATALOG = sorted(
|
||||||
|
CATALOG,
|
||||||
|
key=lambda it: (RARITY_RANK.get(it["rarity"], 99), -float(it["mult"]), it["name"])
|
||||||
|
)
|
||||||
|
# Attach rank (1 = rarest)
|
||||||
|
for i, it in enumerate(SORTED_CATALOG, 1):
|
||||||
|
it["_rank"] = i
|
||||||
|
|
||||||
|
PAGE_SIZE = 20 # 100 items -> 5 pages
|
||||||
|
|
||||||
|
def _collection_page_lines(user_id: int, page: int) -> Tuple[str, int, int, int]:
|
||||||
|
"""
|
||||||
|
Returns (text, start_idx, end_idx, total_pages) for the given page (0-based).
|
||||||
|
"""
|
||||||
|
coll = db.get_packs_collection(user_id)
|
||||||
|
total = len(SORTED_CATALOG)
|
||||||
|
pages = max(1, (total + PAGE_SIZE - 1) // PAGE_SIZE)
|
||||||
|
page = max(0, min(page, pages - 1))
|
||||||
|
start = page * PAGE_SIZE
|
||||||
|
end = min(total, start + PAGE_SIZE)
|
||||||
|
|
||||||
|
lines: List[str] = []
|
||||||
|
for it in SORTED_CATALOG[start:end]:
|
||||||
|
owned = coll.get(it["id"], 0) > 0
|
||||||
|
idx = it["_rank"]
|
||||||
|
if owned:
|
||||||
|
lines.append(f"**#{idx:02d}** {it['emoji']} **{it['name']}** · *{it['rarity']}*")
|
||||||
|
else:
|
||||||
|
lines.append(f"**#{idx:02d}** ?")
|
||||||
|
text = "\n".join(lines)
|
||||||
|
return text, start, end, pages
|
||||||
|
|
||||||
|
def _collection_embed(user_id: int, page: int) -> discord.Embed:
|
||||||
|
coll = db.get_packs_collection(user_id)
|
||||||
|
have = sum(1 for it in SORTED_CATALOG if coll.get(it["id"], 0) > 0)
|
||||||
|
total = len(SORTED_CATALOG)
|
||||||
|
text, start, end, pages = _collection_page_lines(user_id, page)
|
||||||
|
e = discord.Embed(
|
||||||
|
title="🗂️ Packs Collection",
|
||||||
|
description=f"**Progress:** {have}/{total} collected\n"
|
||||||
|
f"Rarest is **#01**, Common end is **#{total:02d}**.\n\n{text}",
|
||||||
|
color=discord.Color.dark_gold()
|
||||||
|
)
|
||||||
|
e.set_footer(text=f"Page {page+1}/{pages} • Showing #{start+1:02d}–#{end:02d}")
|
||||||
|
return e
|
||||||
|
|
||||||
|
# ---------- UI: Set Bet modal ----------
|
||||||
|
class SetBetModal(discord.ui.Modal, title="Set Packs Bet"):
|
||||||
|
def __init__(self, view: "PacksView"):
|
||||||
|
super().__init__()
|
||||||
|
self.view = view
|
||||||
|
self.amount = discord.ui.TextInput(
|
||||||
|
label=f"Amount (min {PACK_MIN_BET})",
|
||||||
|
placeholder=str(self.view.bet),
|
||||||
|
required=True, min_length=1, max_length=10,
|
||||||
|
)
|
||||||
|
self.add_item(self.amount)
|
||||||
|
|
||||||
|
async def on_submit(self, itx: discord.Interaction):
|
||||||
|
if itx.user.id != self.view.user_id:
|
||||||
|
return await itx.response.send_message("This isn’t your Packs panel.", ephemeral=True)
|
||||||
|
if self.view.busy:
|
||||||
|
return await itx.response.send_message("Please wait a moment.", ephemeral=True)
|
||||||
|
raw = str(self.amount.value)
|
||||||
|
try:
|
||||||
|
amt = int("".join(ch for ch in raw if ch.isdigit()))
|
||||||
|
except Exception:
|
||||||
|
return await itx.response.send_message("Enter a valid number.", ephemeral=True)
|
||||||
|
self.view.bet = max(PACK_MIN_BET, amt)
|
||||||
|
await itx.response.defer(ephemeral=True)
|
||||||
|
if self.view.message:
|
||||||
|
await self.view.message.edit(embed=self.view.render(), view=self.view)
|
||||||
|
|
||||||
|
# ---------- UI: Collection View ----------
|
||||||
|
class PacksCollectionView(discord.ui.View):
|
||||||
|
def __init__(self, user_id: int, *, start_page: int = 0, timeout: int = 180):
|
||||||
|
super().__init__(timeout=timeout)
|
||||||
|
self.user_id = user_id
|
||||||
|
self.page = start_page
|
||||||
|
self.pages = max(1, (len(SORTED_CATALOG) + PAGE_SIZE - 1) // PAGE_SIZE)
|
||||||
|
self.message: Optional[discord.Message] = None
|
||||||
|
|
||||||
|
async def on_timeout(self):
|
||||||
|
for c in self.children:
|
||||||
|
c.disabled = True
|
||||||
|
try:
|
||||||
|
if self.message:
|
||||||
|
await self.message.edit(view=self)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def interaction_check(self, interaction: discord.Interaction) -> bool:
|
||||||
|
if interaction.user.id != self.user_id:
|
||||||
|
await interaction.response.send_message("This collection belongs to someone else.", ephemeral=True)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _sync(self):
|
||||||
|
# enable/disable nav buttons based on current page
|
||||||
|
for c in self.children:
|
||||||
|
if isinstance(c, discord.ui.Button):
|
||||||
|
if c.custom_id == "first": c.disabled = (self.page <= 0)
|
||||||
|
if c.custom_id == "prev": c.disabled = (self.page <= 0)
|
||||||
|
if c.custom_id == "next": c.disabled = (self.page >= self.pages - 1)
|
||||||
|
if c.custom_id == "last": c.disabled = (self.page >= self.pages - 1)
|
||||||
|
|
||||||
|
def embed(self) -> discord.Embed:
|
||||||
|
self._sync()
|
||||||
|
return _collection_embed(self.user_id, self.page)
|
||||||
|
|
||||||
|
@discord.ui.button(label="⏮️", style=discord.ButtonStyle.secondary, custom_id="first")
|
||||||
|
async def first(self, itx: discord.Interaction, _):
|
||||||
|
self.page = 0
|
||||||
|
await itx.response.edit_message(embed=self.embed(), view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label="◀️", style=discord.ButtonStyle.secondary, custom_id="prev")
|
||||||
|
async def prev(self, itx: discord.Interaction, _):
|
||||||
|
if self.page > 0:
|
||||||
|
self.page -= 1
|
||||||
|
await itx.response.edit_message(embed=self.embed(), view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label="▶️", style=discord.ButtonStyle.secondary, custom_id="next")
|
||||||
|
async def next(self, itx: discord.Interaction, _):
|
||||||
|
if self.page < self.pages - 1:
|
||||||
|
self.page += 1
|
||||||
|
await itx.response.edit_message(embed=self.embed(), view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label="⏭️", style=discord.ButtonStyle.secondary, custom_id="last")
|
||||||
|
async def last(self, itx: discord.Interaction, _):
|
||||||
|
self.page = self.pages - 1
|
||||||
|
await itx.response.edit_message(embed=self.embed(), view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label="Close", style=discord.ButtonStyle.danger)
|
||||||
|
async def close(self, itx: discord.Interaction, _):
|
||||||
|
try:
|
||||||
|
await itx.message.delete()
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
for c in self.children: c.disabled = True
|
||||||
|
await itx.response.edit_message(view=self)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ---------- Packs game UI ----------
|
||||||
|
class PacksView(discord.ui.View):
|
||||||
|
"""
|
||||||
|
Row 0: Set Bet · ×2 · ½ · Open Pack
|
||||||
|
Row 1: Open Again · Collection
|
||||||
|
"""
|
||||||
|
def __init__(self, user_id: int, initial_bet: int = 100, *, timeout: int = 180):
|
||||||
|
super().__init__(timeout=timeout)
|
||||||
|
self.user_id = user_id
|
||||||
|
self.bet = max(PACK_MIN_BET, initial_bet)
|
||||||
|
self.last_pack: Optional[List[Dict]] = None
|
||||||
|
self.last_return: int = 0
|
||||||
|
self.last_net: int = 0
|
||||||
|
self.busy = False
|
||||||
|
self.message: Optional[discord.Message] = None
|
||||||
|
|
||||||
|
async def on_timeout(self):
|
||||||
|
for c in self.children: c.disabled = True
|
||||||
|
try:
|
||||||
|
if self.message: await self.message.edit(view=self)
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
# ---- render
|
||||||
|
def render(self) -> discord.Embed:
|
||||||
|
color = discord.Color.blurple()
|
||||||
|
if self.last_pack is not None:
|
||||||
|
color = discord.Color.green() if self.last_net > 0 else (discord.Color.red() if self.last_net < 0 else discord.Color.orange())
|
||||||
|
e = discord.Embed(title="📦 Packs", color=color)
|
||||||
|
|
||||||
|
cash, _ = db.get_wallet(self.user_id)
|
||||||
|
desc = [f"**Bet:** {money(self.bet)} • **Balance:** {money(cash)}"]
|
||||||
|
if self.last_pack is None:
|
||||||
|
desc += ["Open a pack of 5 items. Each item pays its multiplier × your bet. Collect them all!"]
|
||||||
|
e.description = "\n".join(desc)
|
||||||
|
|
||||||
|
if self.last_pack is not None:
|
||||||
|
lines = []
|
||||||
|
for it in self.last_pack:
|
||||||
|
tag = "NEW" if it.get("_new") else "DUP"
|
||||||
|
lines.append(f"{it['emoji']} **{it['name']}** · *{it['rarity']}* · ×{it['mult']:.2f} `{tag}`")
|
||||||
|
e.add_field(name="Last Pack", value="\n".join(lines), inline=False)
|
||||||
|
e.add_field(
|
||||||
|
name="Result",
|
||||||
|
value=f"Return: **{money(self.last_return)}** • Net: **{'+' if self.last_net>0 else ''}{money(self.last_net)}**",
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
return e
|
||||||
|
|
||||||
|
# ---- actions
|
||||||
|
async def _open_pack(self, itx: discord.Interaction):
|
||||||
|
if self.busy: return
|
||||||
|
self.busy = True
|
||||||
|
|
||||||
|
cash, _ = db.get_wallet(self.user_id)
|
||||||
|
if self.bet > cash:
|
||||||
|
self.busy = False
|
||||||
|
return await itx.response.send_message("Not enough cash for that bet.", ephemeral=True)
|
||||||
|
|
||||||
|
# debit
|
||||||
|
db.add_cash(self.user_id, -self.bet)
|
||||||
|
|
||||||
|
# current collection to mark NEW/DUP
|
||||||
|
coll = db.get_packs_collection(self.user_id) # dict card_id -> qty
|
||||||
|
|
||||||
|
pulled: List[Dict] = []
|
||||||
|
total_mult = 0.0
|
||||||
|
for _ in range(PACK_SIZE):
|
||||||
|
it = pick_one().copy()
|
||||||
|
it["_new"] = (coll.get(it["id"], 0) == 0)
|
||||||
|
pulled.append(it)
|
||||||
|
total_mult += it["mult"]
|
||||||
|
# persist collection increment
|
||||||
|
db.add_pack_card(self.user_id, it["id"], 1)
|
||||||
|
coll[it["id"]] = coll.get(it["id"], 0) + 1
|
||||||
|
|
||||||
|
returned = int(self.bet * total_mult)
|
||||||
|
if returned > 0:
|
||||||
|
db.add_cash(self.user_id, returned)
|
||||||
|
|
||||||
|
db.record_packs_open(self.user_id, bet=self.bet, return_amount=returned, won=(returned > self.bet))
|
||||||
|
|
||||||
|
self.last_pack = pulled
|
||||||
|
self.last_return = returned
|
||||||
|
self.last_net = returned - self.bet
|
||||||
|
|
||||||
|
await itx.response.edit_message(embed=self.render(), view=self)
|
||||||
|
self.busy = False
|
||||||
|
|
||||||
|
# ----- Buttons
|
||||||
|
@discord.ui.button(label="Set Bet", style=discord.ButtonStyle.secondary, row=0)
|
||||||
|
async def set_bet(self, itx: discord.Interaction, _):
|
||||||
|
if itx.user.id != self.user_id: return await itx.response.send_message("Not your panel.", ephemeral=True)
|
||||||
|
await itx.response.send_modal(SetBetModal(self))
|
||||||
|
|
||||||
|
@discord.ui.button(label="×2", style=discord.ButtonStyle.secondary, row=0)
|
||||||
|
async def x2(self, itx: discord.Interaction, _):
|
||||||
|
if itx.user.id != self.user_id: return await itx.response.send_message("Not your panel.", ephemeral=True)
|
||||||
|
self.bet = max(PACK_MIN_BET, self.bet * 2)
|
||||||
|
await itx.response.edit_message(embed=self.render(), view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label="½", style=discord.ButtonStyle.secondary, row=0)
|
||||||
|
async def half(self, itx: discord.Interaction, _):
|
||||||
|
if itx.user.id != self.user_id: return await itx.response.send_message("Not your panel.", ephemeral=True)
|
||||||
|
self.bet = max(PACK_MIN_BET, self.bet // 2)
|
||||||
|
await itx.response.edit_message(embed=self.render(), view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label="Open Pack", style=discord.ButtonStyle.success, emoji="🎁", row=0)
|
||||||
|
async def open_pack(self, itx: discord.Interaction, _):
|
||||||
|
if itx.user.id != self.user_id: return await itx.response.send_message("Not your panel.", ephemeral=True)
|
||||||
|
await self._open_pack(itx)
|
||||||
|
|
||||||
|
@discord.ui.button(label="Open Again", style=discord.ButtonStyle.primary, emoji="🔁", row=1)
|
||||||
|
async def open_again(self, itx: discord.Interaction, _):
|
||||||
|
if itx.user.id != self.user_id: return await itx.response.send_message("Not your panel.", ephemeral=True)
|
||||||
|
await self._open_pack(itx)
|
||||||
|
|
||||||
|
@discord.ui.button(label="Collection", style=discord.ButtonStyle.secondary, emoji="🗂️", row=1)
|
||||||
|
async def collection(self, itx: discord.Interaction, _):
|
||||||
|
if itx.user.id != self.user_id:
|
||||||
|
return await itx.response.send_message("This isn’t your Packs panel.", ephemeral=True)
|
||||||
|
view = PacksCollectionView(self.user_id, start_page=0, timeout=180)
|
||||||
|
await itx.response.send_message(embed=view.embed(), view=view, ephemeral=True)
|
||||||
|
|
||||||
|
# ---------- Cog ----------
|
||||||
|
class PacksCog(commands.Cog):
|
||||||
|
def __init__(self, bot): self.bot = bot
|
||||||
|
|
||||||
|
@commands.command(name="packs")
|
||||||
|
async def packs(self, ctx: commands.Context, bet: int = None):
|
||||||
|
uid = ctx.author.id
|
||||||
|
view = PacksView(uid, initial_bet=bet or PACK_MIN_BET, timeout=180)
|
||||||
|
msg = await ctx.send(embed=view.render(), view=view)
|
||||||
|
view.message = msg
|
||||||
|
|
||||||
|
@commands.command(name="collection", aliases=["packdex","packsdex"])
|
||||||
|
async def collection(self, ctx: commands.Context):
|
||||||
|
uid = ctx.author.id
|
||||||
|
view = PacksCollectionView(uid, start_page=0, timeout=180)
|
||||||
|
msg = await ctx.send(embed=view.embed(), view=view)
|
||||||
|
view.message = msg
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
# make sure DB tables exist for packs
|
||||||
|
db.ensure_packs_tables()
|
||||||
|
await bot.add_cog(PacksCog(bot))
|
||||||
293
src/db.py
293
src/db.py
@@ -6,11 +6,16 @@ DB_PATH = os.getenv("DB_PATH", "/app/data/blackjack.db")
|
|||||||
def _connect():
|
def _connect():
|
||||||
return sqlite3.connect(DB_PATH)
|
return sqlite3.connect(DB_PATH)
|
||||||
|
|
||||||
|
# --------------------------- bootstrap / migrations ---------------------------
|
||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
|
"""
|
||||||
|
Creates base tables and safely migrates new columns over time.
|
||||||
|
"""
|
||||||
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
||||||
conn = _connect()
|
conn = _connect(); cur = conn.cursor()
|
||||||
cur = conn.cursor()
|
|
||||||
# base
|
# Base users table
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
user_id INTEGER PRIMARY KEY,
|
user_id INTEGER PRIMARY KEY,
|
||||||
@@ -20,43 +25,86 @@ def init_db():
|
|||||||
last_daily DATE DEFAULT NULL
|
last_daily DATE DEFAULT NULL
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
# add new columns if missing
|
|
||||||
|
# Existing columns
|
||||||
cur.execute("PRAGMA table_info(users)")
|
cur.execute("PRAGMA table_info(users)")
|
||||||
cols = {row[1] for row in cur.fetchall()}
|
cols = {row[1] for row in cur.fetchall()}
|
||||||
add_cols = []
|
|
||||||
# shared / economy
|
|
||||||
if "free_spins" not in cols: add_cols.append(("free_spins", "INTEGER DEFAULT 0"))
|
|
||||||
if "last_topup" not in cols: add_cols.append(("last_topup", "TEXT DEFAULT NULL"))
|
|
||||||
if "last_daily_ts" not in cols:add_cols.append(("last_daily_ts", "TEXT DEFAULT NULL"))
|
|
||||||
# blackjack
|
|
||||||
if "games_played_blackjack" not in cols: add_cols.append(("games_played_blackjack", "INTEGER DEFAULT 0"))
|
|
||||||
if "games_won_blackjack" not in cols: add_cols.append(("games_won_blackjack", "INTEGER DEFAULT 0"))
|
|
||||||
# slots
|
|
||||||
if "games_played_slots" not in cols: add_cols.append(("games_played_slots", "INTEGER DEFAULT 0"))
|
|
||||||
if "games_won_slots" not in cols: add_cols.append(("games_won_slots", "INTEGER DEFAULT 0"))
|
|
||||||
if "slots_biggest_win" not in cols: add_cols.append(("slots_biggest_win", "INTEGER DEFAULT 0"))
|
|
||||||
# roulette
|
|
||||||
if "games_played_roulette" not in cols: add_cols.append(("games_played_roulette", "INTEGER DEFAULT 0"))
|
|
||||||
if "roulette_biggest_win" not in cols: add_cols.append(("roulette_biggest_win", "INTEGER DEFAULT 0"))
|
|
||||||
if "roulette_net" not in cols: add_cols.append(("roulette_net", "INTEGER DEFAULT 0"))
|
|
||||||
# coin flip
|
|
||||||
if "games_played_coinflip" not in cols: add_cols.append(("games_played_coinflip", "INTEGER DEFAULT 0"))
|
|
||||||
if "games_won_coinflip" not in cols: add_cols.append(("games_won_coinflip", "INTEGER DEFAULT 0"))
|
|
||||||
if "coinflip_biggest_win" not in cols: add_cols.append(("coinflip_biggest_win", "INTEGER DEFAULT 0"))
|
|
||||||
if "coinflip_net" not in cols: add_cols.append(("coinflip_net", "INTEGER DEFAULT 0"))
|
|
||||||
# towers
|
|
||||||
if "games_played_towers" not in cols: add_cols.append(("games_played_towers", "INTEGER DEFAULT 0"))
|
|
||||||
if "games_won_towers" not in cols: add_cols.append(("games_won_towers", "INTEGER DEFAULT 0"))
|
|
||||||
# baccarat (NEW)
|
|
||||||
if "games_played_baccarat" not in cols: add_cols.append(("games_played_baccarat", "INTEGER DEFAULT 0"))
|
|
||||||
if "games_won_baccarat" not in cols: add_cols.append(("games_won_baccarat", "INTEGER DEFAULT 0"))
|
|
||||||
if "baccarat_net" not in cols: add_cols.append(("baccarat_net", "INTEGER DEFAULT 0"))
|
|
||||||
if "baccarat_biggest_win" not in cols: add_cols.append(("baccarat_biggest_win", "INTEGER DEFAULT 0"))
|
|
||||||
|
|
||||||
for name, decl in add_cols:
|
add: list[tuple[str, str]] = []
|
||||||
|
def need(name: str, ddl: str):
|
||||||
|
"""Queue a column ADD if missing."""
|
||||||
|
if name not in cols:
|
||||||
|
add.append((name, ddl))
|
||||||
|
|
||||||
|
# Shared
|
||||||
|
need("free_spins", "INTEGER DEFAULT 0")
|
||||||
|
need("last_topup", "TEXT DEFAULT NULL")
|
||||||
|
need("last_daily_ts", "TEXT DEFAULT NULL")
|
||||||
|
|
||||||
|
# Blackjack
|
||||||
|
need("games_played_blackjack", "INTEGER DEFAULT 0")
|
||||||
|
need("games_won_blackjack", "INTEGER DEFAULT 0")
|
||||||
|
|
||||||
|
# Slots
|
||||||
|
need("games_played_slots", "INTEGER DEFAULT 0")
|
||||||
|
need("games_won_slots", "INTEGER DEFAULT 0")
|
||||||
|
need("slots_biggest_win", "INTEGER DEFAULT 0")
|
||||||
|
|
||||||
|
# Roulette
|
||||||
|
need("games_played_roulette", "INTEGER DEFAULT 0")
|
||||||
|
need("roulette_biggest_win", "INTEGER DEFAULT 0")
|
||||||
|
need("roulette_net", "INTEGER DEFAULT 0")
|
||||||
|
|
||||||
|
# Coin Flip
|
||||||
|
need("games_played_coinflip", "INTEGER DEFAULT 0")
|
||||||
|
need("games_won_coinflip", "INTEGER DEFAULT 0")
|
||||||
|
need("coinflip_biggest_win", "INTEGER DEFAULT 0")
|
||||||
|
need("coinflip_net", "INTEGER DEFAULT 0")
|
||||||
|
|
||||||
|
# Towers
|
||||||
|
need("games_played_towers", "INTEGER DEFAULT 0")
|
||||||
|
need("games_won_towers", "INTEGER DEFAULT 0")
|
||||||
|
|
||||||
|
# Baccarat
|
||||||
|
need("games_played_baccarat", "INTEGER DEFAULT 0")
|
||||||
|
need("games_won_baccarat", "INTEGER DEFAULT 0")
|
||||||
|
need("baccarat_net", "INTEGER DEFAULT 0")
|
||||||
|
need("baccarat_biggest_win", "INTEGER DEFAULT 0")
|
||||||
|
|
||||||
|
# Hi-Lo
|
||||||
|
need("games_played_hilo", "INTEGER DEFAULT 0")
|
||||||
|
need("games_won_hilo", "INTEGER DEFAULT 0")
|
||||||
|
need("hilo_net", "INTEGER DEFAULT 0")
|
||||||
|
need("hilo_biggest_win", "INTEGER DEFAULT 0")
|
||||||
|
|
||||||
|
# Mines
|
||||||
|
need("games_played_mines", "INTEGER DEFAULT 0")
|
||||||
|
need("games_won_mines", "INTEGER DEFAULT 0")
|
||||||
|
need("mines_net", "INTEGER DEFAULT 0")
|
||||||
|
need("mines_biggest_win", "INTEGER DEFAULT 0")
|
||||||
|
|
||||||
|
# Packs
|
||||||
|
need("games_played_packs", "INTEGER DEFAULT 0")
|
||||||
|
need("games_won_packs", "INTEGER DEFAULT 0")
|
||||||
|
need("packs_net", "INTEGER DEFAULT 0")
|
||||||
|
need("packs_biggest_win", "INTEGER DEFAULT 0")
|
||||||
|
|
||||||
|
# Apply queued column ADDs
|
||||||
|
for name, decl in add:
|
||||||
cur.execute(f"ALTER TABLE users ADD COLUMN {name} {decl}")
|
cur.execute(f"ALTER TABLE users ADD COLUMN {name} {decl}")
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
# Collections table for Packs
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS packs_user_cards (
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
card_id TEXT NOT NULL,
|
||||||
|
qty INTEGER NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (user_id, card_id)
|
||||||
|
)""")
|
||||||
|
|
||||||
|
conn.commit(); conn.close()
|
||||||
|
|
||||||
|
# ------------------------------ user & wallet --------------------------------
|
||||||
|
|
||||||
def ensure_user(user_id: int):
|
def ensure_user(user_id: int):
|
||||||
conn = _connect(); cur = conn.cursor()
|
conn = _connect(); cur = conn.cursor()
|
||||||
@@ -96,10 +144,10 @@ def consume_free_spin(user_id: int) -> bool:
|
|||||||
cur.execute("UPDATE users SET free_spins = free_spins - 1 WHERE user_id=?", (user_id,))
|
cur.execute("UPDATE users SET free_spins = free_spins - 1 WHERE user_id=?", (user_id,))
|
||||||
conn.commit(); conn.close()
|
conn.commit(); conn.close()
|
||||||
return True
|
return True
|
||||||
conn.close()
|
conn.close(); return False
|
||||||
return False
|
|
||||||
|
# ----------------------------- generic counters ------------------------------
|
||||||
|
|
||||||
# legacy aggregate
|
|
||||||
def record_game(user_id: int, won: bool):
|
def record_game(user_id: int, won: bool):
|
||||||
ensure_user(user_id)
|
ensure_user(user_id)
|
||||||
conn = _connect(); cur = conn.cursor()
|
conn = _connect(); cur = conn.cursor()
|
||||||
@@ -109,7 +157,8 @@ def record_game(user_id: int, won: bool):
|
|||||||
cur.execute("UPDATE users SET games_played=games_played+1 WHERE user_id=?", (user_id,))
|
cur.execute("UPDATE users SET games_played=games_played+1 WHERE user_id=?", (user_id,))
|
||||||
conn.commit(); conn.close()
|
conn.commit(); conn.close()
|
||||||
|
|
||||||
# per-game
|
# ------------------------------ per-game stats -------------------------------
|
||||||
|
|
||||||
def record_blackjack(user_id: int, won: bool):
|
def record_blackjack(user_id: int, won: bool):
|
||||||
ensure_user(user_id)
|
ensure_user(user_id)
|
||||||
conn = _connect(); cur = conn.cursor()
|
conn = _connect(); cur = conn.cursor()
|
||||||
@@ -187,7 +236,6 @@ def record_coinflip(user_id: int, bet: int, return_amount: int, won: bool):
|
|||||||
record_game(user_id, won)
|
record_game(user_id, won)
|
||||||
|
|
||||||
def record_towers(user_id: int, won: bool):
|
def record_towers(user_id: int, won: bool):
|
||||||
"""Call with won=True on cashout/full clear; won=False on bomb."""
|
|
||||||
ensure_user(user_id)
|
ensure_user(user_id)
|
||||||
conn = _connect(); cur = conn.cursor()
|
conn = _connect(); cur = conn.cursor()
|
||||||
if won:
|
if won:
|
||||||
@@ -206,13 +254,7 @@ def record_towers(user_id: int, won: bool):
|
|||||||
conn.commit(); conn.close()
|
conn.commit(); conn.close()
|
||||||
record_game(user_id, won)
|
record_game(user_id, won)
|
||||||
|
|
||||||
# NEW: Baccarat
|
|
||||||
def record_baccarat(user_id: int, total_bet: int, total_return: int):
|
def record_baccarat(user_id: int, total_bet: int, total_return: int):
|
||||||
"""
|
|
||||||
Record a single baccarat hand.
|
|
||||||
- total_bet: sum of Player+Banker+Tie stakes debited
|
|
||||||
- total_return: total chips returned (including stake(s) on pushes/wins)
|
|
||||||
"""
|
|
||||||
ensure_user(user_id)
|
ensure_user(user_id)
|
||||||
net = total_return - total_bet
|
net = total_return - total_bet
|
||||||
won = net > 0
|
won = net > 0
|
||||||
@@ -228,45 +270,178 @@ def record_baccarat(user_id: int, total_bet: int, total_return: int):
|
|||||||
conn.commit(); conn.close()
|
conn.commit(); conn.close()
|
||||||
record_game(user_id, won)
|
record_game(user_id, won)
|
||||||
|
|
||||||
|
def record_hilo(user_id: int, bet: int, return_amount: int, won: bool):
|
||||||
|
"""
|
||||||
|
Hi-Lo round:
|
||||||
|
- bet debited up front
|
||||||
|
- return_amount received on cashout (0 on loss)
|
||||||
|
- won for W/L if return > bet
|
||||||
|
"""
|
||||||
|
ensure_user(user_id)
|
||||||
|
net = return_amount - bet
|
||||||
|
conn = _connect(); cur = conn.cursor()
|
||||||
|
if won:
|
||||||
|
cur.execute("""
|
||||||
|
UPDATE users
|
||||||
|
SET games_played_hilo = games_played_hilo + 1,
|
||||||
|
games_won_hilo = games_won_hilo + 1,
|
||||||
|
hilo_net = hilo_net + ?,
|
||||||
|
hilo_biggest_win = CASE WHEN ? > hilo_biggest_win THEN ? ELSE hilo_biggest_win END
|
||||||
|
WHERE user_id = ?
|
||||||
|
""", (net, return_amount, return_amount, user_id))
|
||||||
|
else:
|
||||||
|
cur.execute("""
|
||||||
|
UPDATE users
|
||||||
|
SET games_played_hilo = games_played_hilo + 1,
|
||||||
|
hilo_net = hilo_net + ?
|
||||||
|
WHERE user_id = ?
|
||||||
|
""", (net, user_id))
|
||||||
|
conn.commit(); conn.close()
|
||||||
|
record_game(user_id, won)
|
||||||
|
|
||||||
|
def record_mines(user_id: int, bet: int, return_amount: int, won: bool):
|
||||||
|
ensure_user(user_id)
|
||||||
|
net = return_amount - bet
|
||||||
|
conn = _connect(); cur = conn.cursor()
|
||||||
|
if won:
|
||||||
|
cur.execute("""
|
||||||
|
UPDATE users
|
||||||
|
SET games_played_mines = games_played_mines + 1,
|
||||||
|
games_won_mines = games_won_mines + 1,
|
||||||
|
mines_net = mines_net + ?,
|
||||||
|
mines_biggest_win = CASE WHEN ? > mines_biggest_win THEN ? ELSE mines_biggest_win END
|
||||||
|
WHERE user_id = ?
|
||||||
|
""", (net, return_amount, return_amount, user_id))
|
||||||
|
else:
|
||||||
|
cur.execute("""
|
||||||
|
UPDATE users
|
||||||
|
SET games_played_mines = games_played_mines + 1,
|
||||||
|
mines_net = mines_net + ?
|
||||||
|
WHERE user_id = ?
|
||||||
|
""", (net, user_id))
|
||||||
|
conn.commit(); conn.close()
|
||||||
|
record_game(user_id, won)
|
||||||
|
|
||||||
|
# ---------------------------------- Packs ------------------------------------
|
||||||
|
|
||||||
|
def ensure_packs_tables():
|
||||||
|
conn = _connect(); cur = conn.cursor()
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS packs_user_cards (
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
card_id TEXT NOT NULL,
|
||||||
|
qty INTEGER NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (user_id, card_id)
|
||||||
|
)""")
|
||||||
|
conn.commit(); conn.close()
|
||||||
|
|
||||||
|
def add_pack_card(user_id: int, card_id: str, qty: int = 1):
|
||||||
|
ensure_user(user_id)
|
||||||
|
ensure_packs_tables()
|
||||||
|
conn = _connect(); cur = conn.cursor()
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO packs_user_cards (user_id, card_id, qty)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
ON CONFLICT(user_id, card_id) DO UPDATE SET qty = qty + excluded.qty
|
||||||
|
""", (user_id, card_id, qty))
|
||||||
|
conn.commit(); conn.close()
|
||||||
|
|
||||||
|
def get_packs_collection(user_id: int) -> dict:
|
||||||
|
ensure_user(user_id)
|
||||||
|
ensure_packs_tables()
|
||||||
|
conn = _connect(); cur = conn.cursor()
|
||||||
|
cur.execute("SELECT card_id, qty FROM packs_user_cards WHERE user_id = ?", (user_id,))
|
||||||
|
out = {row[0]: row[1] for row in cur.fetchall()}
|
||||||
|
conn.close()
|
||||||
|
return out
|
||||||
|
|
||||||
|
def record_packs_open(user_id: int, bet: int, return_amount: int, won: bool):
|
||||||
|
ensure_user(user_id)
|
||||||
|
net = return_amount - bet
|
||||||
|
conn = _connect(); cur = conn.cursor()
|
||||||
|
if won:
|
||||||
|
cur.execute("""
|
||||||
|
UPDATE users
|
||||||
|
SET games_played_packs = games_played_packs + 1,
|
||||||
|
games_won_packs = games_won_packs + 1,
|
||||||
|
packs_net = packs_net + ?,
|
||||||
|
packs_biggest_win = CASE WHEN ? > packs_biggest_win THEN ? ELSE packs_biggest_win END
|
||||||
|
WHERE user_id = ?
|
||||||
|
""", (net, return_amount, return_amount, user_id))
|
||||||
|
else:
|
||||||
|
cur.execute("""
|
||||||
|
UPDATE users
|
||||||
|
SET games_played_packs = games_played_packs + 1,
|
||||||
|
packs_net = packs_net + ?
|
||||||
|
WHERE user_id = ?
|
||||||
|
""", (net, user_id))
|
||||||
|
conn.commit(); conn.close()
|
||||||
|
record_game(user_id, won)
|
||||||
|
|
||||||
|
# ------------------------------ queries & misc -------------------------------
|
||||||
|
|
||||||
def top_cash(limit=10):
|
def top_cash(limit=10):
|
||||||
conn = _connect(); cur = conn.cursor()
|
conn = _connect(); cur = conn.cursor()
|
||||||
cur.execute("SELECT user_id, cash FROM users ORDER BY cash DESC LIMIT ?", (limit,))
|
cur.execute("SELECT user_id, cash FROM users ORDER BY cash DESC LIMIT ?", (limit,))
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall(); conn.close()
|
||||||
conn.close()
|
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
def user_counts():
|
def user_counts():
|
||||||
conn = _connect(); cur = conn.cursor()
|
conn = _connect(); cur = conn.cursor()
|
||||||
cur.execute("SELECT COUNT(*) FROM users")
|
cur.execute("SELECT COUNT(*) FROM users"); n = cur.fetchone()[0]
|
||||||
n = cur.fetchone()[0]
|
conn.close(); return n
|
||||||
conn.close()
|
|
||||||
return n
|
|
||||||
|
|
||||||
def get_full_stats(user_id: int):
|
def get_full_stats(user_id: int):
|
||||||
ensure_user(user_id)
|
ensure_user(user_id)
|
||||||
conn = _connect(); cur = conn.cursor()
|
conn = _connect(); cur = conn.cursor()
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT cash, free_spins, games_played, games_won,
|
SELECT cash, free_spins, games_played, games_won,
|
||||||
|
|
||||||
games_played_blackjack, games_won_blackjack,
|
games_played_blackjack, games_won_blackjack,
|
||||||
|
|
||||||
games_played_slots, games_won_slots,
|
games_played_slots, games_won_slots,
|
||||||
|
|
||||||
games_played_roulette, roulette_net,
|
games_played_roulette, roulette_net,
|
||||||
|
|
||||||
games_played_coinflip, games_won_coinflip, coinflip_net,
|
games_played_coinflip, games_won_coinflip, coinflip_net,
|
||||||
|
|
||||||
games_played_towers, games_won_towers,
|
games_played_towers, games_won_towers,
|
||||||
games_played_baccarat, games_won_baccarat, baccarat_net
|
|
||||||
|
games_played_baccarat, games_won_baccarat, baccarat_net,
|
||||||
|
|
||||||
|
games_played_hilo, games_won_hilo, hilo_net,
|
||||||
|
|
||||||
|
games_played_mines, games_won_mines, mines_net,
|
||||||
|
|
||||||
|
games_played_packs, games_won_packs, packs_net
|
||||||
FROM users WHERE user_id=?
|
FROM users WHERE user_id=?
|
||||||
""", (user_id,))
|
""", (user_id,))
|
||||||
row = cur.fetchone(); conn.close()
|
row = cur.fetchone(); conn.close()
|
||||||
keys = [
|
keys = [
|
||||||
"cash","free_spins","games_played","games_won",
|
"cash","free_spins","games_played","games_won",
|
||||||
|
|
||||||
"gp_bj","gw_bj",
|
"gp_bj","gw_bj",
|
||||||
|
|
||||||
"gp_slots","gw_slots",
|
"gp_slots","gw_slots",
|
||||||
|
|
||||||
"gp_roulette","roulette_net",
|
"gp_roulette","roulette_net",
|
||||||
|
|
||||||
"gp_coin","gw_coin","coin_net",
|
"gp_coin","gw_coin","coin_net",
|
||||||
|
|
||||||
"gp_towers","gw_towers",
|
"gp_towers","gw_towers",
|
||||||
|
|
||||||
"gp_bac","gw_bac","bac_net",
|
"gp_bac","gw_bac","bac_net",
|
||||||
|
|
||||||
|
"gp_hilo","gw_hilo","hilo_net",
|
||||||
|
|
||||||
|
"gp_mines","gw_mines","mines_net",
|
||||||
|
|
||||||
|
"gp_packs","gw_packs","packs_net",
|
||||||
]
|
]
|
||||||
return dict(zip(keys, row))
|
return dict(zip(keys, row))
|
||||||
|
|
||||||
|
# -------------------------- faucet / daily / transfer -------------------------
|
||||||
|
|
||||||
def can_claim_topup(user_id: int, cooldown_minutes: int = 5) -> tuple[bool, int]:
|
def can_claim_topup(user_id: int, cooldown_minutes: int = 5) -> tuple[bool, int]:
|
||||||
ensure_user(user_id)
|
ensure_user(user_id)
|
||||||
conn = _connect(); cur = conn.cursor()
|
conn = _connect(); cur = conn.cursor()
|
||||||
@@ -276,8 +451,7 @@ def can_claim_topup(user_id: int, cooldown_minutes: int = 5) -> tuple[bool, int]
|
|||||||
if not last: return True, 0
|
if not last: return True, 0
|
||||||
try: last_dt = datetime.fromisoformat(last)
|
try: last_dt = datetime.fromisoformat(last)
|
||||||
except Exception: return True, 0
|
except Exception: return True, 0
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow(); wait = timedelta(minutes=cooldown_minutes)
|
||||||
wait = timedelta(minutes=cooldown_minutes)
|
|
||||||
if now - last_dt >= wait: return True, 0
|
if now - last_dt >= wait: return True, 0
|
||||||
left = int((wait - (now - last_dt)).total_seconds())
|
left = int((wait - (now - last_dt)).total_seconds())
|
||||||
return False, left
|
return False, left
|
||||||
@@ -298,16 +472,14 @@ def daily_cooldown(user_id: int, hours: int = 24) -> tuple[bool, int]:
|
|||||||
if not last: return True, 0
|
if not last: return True, 0
|
||||||
try: last_dt = datetime.fromisoformat(last)
|
try: last_dt = datetime.fromisoformat(last)
|
||||||
except Exception: return True, 0
|
except Exception: return True, 0
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow(); wait = timedelta(hours=hours)
|
||||||
wait = timedelta(hours=hours)
|
|
||||||
if now - last_dt >= wait: return True, 0
|
if now - last_dt >= wait: return True, 0
|
||||||
secs_left = int((wait - (now - last_dt)).total_seconds())
|
secs_left = int((wait - (now - last_dt)).total_seconds())
|
||||||
return False, secs_left
|
return False, secs_left
|
||||||
|
|
||||||
def claim_daily(user_id: int, cash_bonus: int, spin_bonus: int):
|
def claim_daily(user_id: int, cash_bonus: int, spin_bonus: int):
|
||||||
ensure_user(user_id)
|
ensure_user(user_id)
|
||||||
today = str(date.today())
|
today = str(date.today()); now_str = datetime.utcnow().isoformat(timespec="seconds")
|
||||||
now_str = datetime.utcnow().isoformat(timespec="seconds")
|
|
||||||
conn = _connect(); cur = conn.cursor()
|
conn = _connect(); cur = conn.cursor()
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
UPDATE users
|
UPDATE users
|
||||||
@@ -337,4 +509,3 @@ def transfer_cash(from_user_id: int, to_user_id: int, amount: int):
|
|||||||
conn.rollback(); raise
|
conn.rollback(); raise
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|||||||
@@ -54,14 +54,25 @@ ROULETTE_PAYOUTS = {
|
|||||||
"lowhigh": 1, # K=9 (0 loses)
|
"lowhigh": 1, # K=9 (0 loses)
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Coin Flip constants ---
|
# Coin flip Tuning
|
||||||
|
|
||||||
COINFLIP_MIN_CHIP = int(os.getenv("COINFLIP_MIN_CHIP", 10))
|
COINFLIP_MIN_CHIP = int(os.getenv("COINFLIP_MIN_CHIP", 10))
|
||||||
COINFLIP_MIN_BET = int(os.getenv("COINFLIP_MIN_BET", COINFLIP_MIN_CHIP))
|
COINFLIP_MIN_BET = int(os.getenv("COINFLIP_MIN_BET", COINFLIP_MIN_CHIP))
|
||||||
COINFLIP_MAX_BET = int(os.getenv("COINFLIP_MAX_BET", 50000))
|
COINFLIP_MAX_BET = int(os.getenv("COINFLIP_MAX_BET", 50000))
|
||||||
|
|
||||||
# --- Towers constants ---
|
# Towers tuning
|
||||||
TOWERS_MIN_BET = int(os.getenv("TOWERS_MIN_BET", 10))
|
TOWERS_MIN_BET = int(os.getenv("TOWERS_MIN_BET", 10))
|
||||||
TOWERS_MAX_BET = int(os.getenv("TOWERS_MAX_BET", 50000))
|
TOWERS_MAX_BET = int(os.getenv("TOWERS_MAX_BET", 50000))
|
||||||
TOWERS_EDGE_PER_STEP = float(os.getenv("TOWERS_EDGE_PER_STEP", 0.02))
|
TOWERS_EDGE_PER_STEP = float(os.getenv("TOWERS_EDGE_PER_STEP", 0.02))
|
||||||
|
|
||||||
|
# Hi-Lo tuning
|
||||||
|
HILO_EDGE_PER_STEP = 0.02 # 2% edge per correct step
|
||||||
|
HILO_MAX_MULT = 50.0 # cap total multiplier to avoid runaway
|
||||||
|
HILO_MIN_BET = 10
|
||||||
|
|
||||||
|
# Mines tuning
|
||||||
|
MINES_MIN_BET = 10
|
||||||
|
MINES_EDGE_PER_STEP = 0.015
|
||||||
|
MINES_MAX_MULT = 1000.0
|
||||||
|
MINES_CHOICES = [1, 3, 5, 7]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user