Initial commit: Casino bot

This commit is contained in:
2025-08-28 02:51:39 -05:00
commit 45afb95d14
19 changed files with 3119 additions and 0 deletions

12
.env.example Normal file
View File

@@ -0,0 +1,12 @@
# Discord Bot
DISCORD_TOKEN=YOUR_BOT_TOKEN_HERE
# Database
DB_PATH=/app/data/blackjack.db
# Economy tuning (optional)
DAILY_CASH=10000
DAILY_FREE_SPINS=3
TOPUP_AMOUNT=100
TOPUP_COOLDOWN_MINUTES=5
DAILY_COOLDOWN_HOURS=24

14
.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
# Python
__pycache__/
*.pyc
.venv/
.env
.env.local
# OS
.DS_Store
# Project
data/
*.db
backups/
# Docker
*.log

21
Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
FROM python:3.11-slim
WORKDIR /app
RUN apt-get update && apt-get install -y sqlite3 && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY src ./src
RUN mkdir -p /app/data
# non-root
RUN useradd -r -s /bin/false botuser && chown -R botuser:botuser /app
USER botuser
# healthcheck: open/close DB
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import sqlite3; sqlite3.connect('/app/data/blackjack.db').close()" || exit 1
CMD ["python", "-m", "src.bot"]

7
README.md Normal file
View File

@@ -0,0 +1,7 @@
Blackjack, Slots, Mini Roulette, Coin Flip, Towers, Baccarat — all-in-one Discord bot.
## Quick start (Docker)
```bash
cp .env.example .env
# edit .env and paste your DISCORD_TOKEN
docker compose up -d --build

22
docker-compose.yml Normal file
View File

@@ -0,0 +1,22 @@
services:
casino-bot:
build: .
container_name: discord-casino-bot
restart: unless-stopped
env_file: .env
user: "${UID:-1000}:${GID:-1000}"
environment:
- PYTHONUNBUFFERED=1
- DB_PATH=/app/data/blackjack.db
volumes:
- ./data:/app/data
healthcheck:
test: ["CMD", "python", "-c", "import sqlite3; sqlite3.connect('/app/data/blackjack.db').close()"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
logging:
driver: "json-file"
options: { max-size: "10m", max-file: "3" }

2
requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
discord.py==2.3.2
aiohttp==3.9.1

0
src/__init__.py Normal file
View File

35
src/bot.py Normal file
View File

@@ -0,0 +1,35 @@
import os, asyncio, discord
from discord.ext import commands
from . import db
intents = discord.Intents.default()
intents.message_content = True
intents.members = True # better name resolution
BOT_PREFIX = os.getenv("BOT_PREFIX", "!")
TOKEN = os.getenv("DISCORD_TOKEN")
bot = commands.Bot(command_prefix=BOT_PREFIX, intents=intents)
@bot.event
async def on_ready():
print(f"🎯 {bot.user} has connected to Discord!")
print(f"🔗 Bot is in {len(bot.guilds)} servers")
async def main():
if not TOKEN:
print("❌ DISCORD_TOKEN environment variable not found!"); raise SystemExit(1)
db.init_db()
# Load cogs
await bot.load_extension("src.cogs.economy")
await bot.load_extension("src.cogs.blackjack")
await bot.load_extension("src.cogs.slots")
await bot.load_extension("src.cogs.roulette")
await bot.load_extension("src.cogs.coinflip")
await bot.load_extension("src.cogs.towers")
await bot.load_extension("src.cogs.baccarat")
await bot.start(TOKEN)
if __name__ == "__main__":
asyncio.run(main())

0
src/cogs/__init__.py Normal file
View File

337
src/cogs/baccarat.py Normal file
View File

@@ -0,0 +1,337 @@
# src/cogs/baccarat.py
import random, discord
from discord.ext import commands
from typing import Optional, List, Tuple, Dict
from .. import db
# ====== Config (inline so you don't have to touch constants.py) ======
BAC_MIN_BET = 10
BAC_MAX_BET = 100_000
TIE_PAYS = 8 # 8:1 (plus stake => ×9 return)
BANKER_COMMISSION = 0.05 # 5%
# ====== Cards / shoe ======
SUITS = ["♠️","♥️","♦️","♣️"]
RANKS = ["A","2","3","4","5","6","7","8","9","10","J","Q","K"]
def baccarat_val(rank: str) -> int:
if rank == "A": return 1
if rank in ("10","J","Q","K"): return 0
return int(rank)
def total(hand: List[Tuple[str,str]]) -> int:
return sum(baccarat_val(r) for _, r in hand) % 10
class Shoe:
"""Simple 8-deck shoe; rebuild when low."""
def __init__(self, decks: int = 8):
self.decks = decks
self.cards: List[Tuple[str,str]] = []
self._build()
def _build(self):
self.cards = [(s, r) for _ in range(self.decks) for s in SUITS for r in RANKS]
random.shuffle(self.cards)
def draw(self) -> Tuple[str,str]:
if len(self.cards) < 12:
self._build()
return self.cards.pop()
# ====== Third-card rules ======
def deal_baccarat_round(shoe: Shoe):
"""Return dict with hands, totals, winner, draws detail."""
player: List[Tuple[str,str]] = [shoe.draw(), shoe.draw()]
banker: List[Tuple[str,str]] = [shoe.draw(), shoe.draw()]
pt = total(player)
bt = total(banker)
natural = (pt >= 8 or bt >= 8)
player_third = None
banker_third = None
if not natural:
# Player rule
if pt <= 5:
player_third = shoe.draw()
player.append(player_third)
pt = total(player)
# Banker rules depend on whether player drew
if player_third is None:
# Player stood
if bt <= 5:
banker_third = shoe.draw()
banker.append(banker_third)
bt = total(banker)
else:
# Player drew: use banker conditional table on player's third rank value
pv = baccarat_val(player_third[1])
if bt <= 2:
banker_third = shoe.draw()
banker.append(banker_third)
bt = total(banker)
elif bt == 3 and pv != 8:
banker_third = shoe.draw(); banker.append(banker_third); bt = total(banker)
elif bt == 4 and pv in (2,3,4,5,6,7):
banker_third = shoe.draw(); banker.append(banker_third); bt = total(banker)
elif bt == 5 and pv in (4,5,6,7):
banker_third = shoe.draw(); banker.append(banker_third); bt = total(banker)
elif bt == 6 and pv in (6,7):
banker_third = shoe.draw(); banker.append(banker_third); bt = total(banker)
# bt==7 stands
# Determine result
winner: str
if pt > bt:
winner = "PLAYER"
elif bt > pt:
winner = "BANKER"
else:
winner = "TIE"
return {
"player": player, "banker": banker,
"pt": pt, "bt": bt, "winner": winner,
"player_third": player_third, "banker_third": banker_third,
}
def fmt_hand(hand: List[Tuple[str,str]]) -> str:
return " ".join(f"{r}{s}" for s, r in hand)
def money(n: int) -> str:
return f"${n:,}"
# ====== UI ======
_active_bac: Dict[int, "BaccaratView"] = {}
class SetBetModal(discord.ui.Modal):
def __init__(self, view: "BaccaratView", label: str, target: str):
super().__init__(title=f"Set {label} Bet")
self.view = view
self.target = target # "player" | "tie" | "banker"
self.amount = discord.ui.TextInput(
label=f"Amount (min {BAC_MIN_BET}, max {BAC_MAX_BET})",
placeholder=str(getattr(view, f'bet_{target}')),
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 isnt your baccarat panel.", ephemeral=True)
if self.view.in_round:
return await itx.response.send_message("Wait for the current deal to finish.", 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)
amt = max(BAC_MIN_BET, min(BAC_MAX_BET, amt))
setattr(self.view, f"bet_{self.target}", amt)
await itx.response.defer(ephemeral=True)
if self.view.message:
await self.view.message.edit(embed=self.view.panel_embed(), view=self.view)
class BaccaratView(discord.ui.View):
"""
Row 0: Player Bet • Tie Bet • Banker Bet • Deal
"""
def __init__(self, user_id: int, *, timeout: int = 180):
super().__init__(timeout=timeout)
self.user_id = user_id
self.shoe = Shoe(8)
self.bet_player = 0
self.bet_tie = 0
self.bet_banker = 0
self.in_round = False
self.last_outcome: Optional[str] = None
self.last_detail: Optional[str] = None
self.last_player: List[Tuple[str,str]] = []
self.last_banker: List[Tuple[str,str]] = []
self.last_pt = 0
self.last_bt = 0
self.message: Optional[discord.Message] = None
# ----- 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)
finally:
_active_bac.pop(self.user_id, None)
# ----- render -----
def panel_embed(self) -> discord.Embed:
cash, _ = db.get_wallet(self.user_id)
e = discord.Embed(title="🃠 Baccarat", color=discord.Color.dark_green())
lines = [
f"**Bets:** Player {money(self.bet_player)} • Tie {money(self.bet_tie)} • Banker {money(self.bet_banker)}",
f"**Balance:** {money(cash)}",
"",
]
if self.last_outcome:
board = (
f"**Player** ({self.last_pt}) — {fmt_hand(self.last_player)}\n"
f"**Banker** ({self.last_bt}) — {fmt_hand(self.last_banker)}\n"
f"**Result:** **{self.last_outcome}**"
)
lines.append(board)
if self.last_detail:
lines.append(self.last_detail)
e.description = "\n".join(lines)
e.set_footer(text="Set your bets, then Deal. Player pays 1:1 • Banker 1:1 (5% commission) • Tie 8:1 (Player/Banker push on tie)")
return e
# ----- helpers -----
def total_stake(self) -> int:
return max(0, self.bet_player) + max(0, self.bet_tie) + max(0, self.bet_banker)
async def _ensure_owner(self, itx: discord.Interaction):
if itx.user.id != self.user_id:
raise await itx.response.send_message("This isnt your baccarat panel.", ephemeral=True)
async def _deal_once(self, itx: discord.Interaction):
# Validate stake
total = self.total_stake()
if total <= 0:
return await itx.response.send_message("Place a bet first (Player, Tie, or Banker).", ephemeral=True)
cash, _ = db.get_wallet(self.user_id)
if cash < total:
return await itx.response.send_message(f"Not enough cash for total stake {money(total)}.", ephemeral=True)
# Debit upfront
db.add_cash(self.user_id, -total)
self.in_round = True
await itx.response.edit_message(embed=self.panel_embed(), view=self)
# Deal & settle
res = deal_baccarat_round(self.shoe)
self.last_player = res["player"]
self.last_banker = res["banker"]
self.last_pt = res["pt"]
self.last_bt = res["bt"]
winner = res["winner"]
self.last_outcome = winner.title()
# Compute returns
returned = 0
won_any = False
if winner == "PLAYER":
if self.bet_player > 0:
returned += self.bet_player * 2 # stake + 1:1
won_any = True
# Banker bet loses; Tie loses
elif winner == "BANKER":
if self.bet_banker > 0:
profit = int(round(self.bet_banker * (1.0 - BANKER_COMMISSION)))
returned += self.bet_banker + profit # stake + profit (0.95x)
won_any = True
else: # TIE
if self.bet_tie > 0:
returned += self.bet_tie * (TIE_PAYS + 1) # stake + 8x
won_any = True
# Player/Banker push on tie
returned += self.bet_player + self.bet_banker
if returned:
db.add_cash(self.user_id, returned)
net = returned - total
if net > 0:
self.last_detail = f"💰 Returned **{money(returned)}** • Net **+{money(net)}**"
elif net < 0:
self.last_detail = f"💸 Returned **{money(returned)}** • Net **-{money(-net)}**"
else:
self.last_detail = f"↔️ Returned **{money(returned)}** • **Push**"
# Record overall game W/L based on net
try:
db.record_baccarat(self.user_id, total_bet=total, total_return=returned)
except Exception:
pass
# Round finished
self.in_round = False
# Re-render
if self.message:
await self.message.edit(embed=self.panel_embed(), view=self)
# ----- controls (Row 0) -----
@discord.ui.button(label="Player Bet", style=discord.ButtonStyle.secondary, row=0)
async def set_p(self, itx: discord.Interaction, _):
await self._ensure_owner(itx)
if self.in_round: return await itx.response.send_message("Wait for the current deal to finish.", ephemeral=True)
await itx.response.send_modal(SetBetModal(self, "Player", "player"))
@discord.ui.button(label="Tie Bet", style=discord.ButtonStyle.secondary, row=0)
async def set_t(self, itx: discord.Interaction, _):
await self._ensure_owner(itx)
if self.in_round: return await itx.response.send_message("Wait for the current deal to finish.", ephemeral=True)
await itx.response.send_modal(SetBetModal(self, "Tie", "tie"))
@discord.ui.button(label="Banker Bet", style=discord.ButtonStyle.secondary, row=0)
async def set_b(self, itx: discord.Interaction, _):
await self._ensure_owner(itx)
if self.in_round: return await itx.response.send_message("Wait for the current deal to finish.", ephemeral=True)
await itx.response.send_modal(SetBetModal(self, "Banker", "banker"))
@discord.ui.button(label="Deal", style=discord.ButtonStyle.success, emoji="🃏", row=0)
async def deal(self, itx: discord.Interaction, _):
await self._ensure_owner(itx)
if self.in_round: return await itx.response.send_message("Already dealing…", ephemeral=True)
await self._deal_on_click(itx)
async def _deal_on_click(self, itx: discord.Interaction):
try:
await self._deal_once(itx)
except Exception as e:
await itx.followup.send(f"⚠️ Error during deal: `{type(e).__name__}: {e}`", ephemeral=True)
class Baccarat(commands.Cog):
def __init__(self, bot): self.bot = bot
@commands.command(name="baccarat", aliases=["bac"])
async def baccarat(self, ctx: commands.Context):
uid = ctx.author.id
if uid in _active_bac:
# Replace their old panel if it's still around
try:
old = _active_bac[uid]
if old.message: await old.message.delete()
except Exception: pass
_active_bac.pop(uid, None)
view = BaccaratView(uid, timeout=180)
msg = await ctx.send(embed=view.panel_embed(), view=view)
view.message = msg
_active_bac[uid] = view
@commands.command(name="rules_baccarat", aliases=["baccarat_rules"])
async def rules_baccarat(self, ctx: commands.Context):
e = discord.Embed(title="🃠 Baccarat — Rules", color=discord.Color.dark_green())
e.description = (
"**Goal:** Bet on which hand totals closest to 9 (mod 10). Aces=1, 10/J/Q/K=0, others=face value.\n\n"
"**Bets:** **Player** (1:1), **Banker** (1:1 less 5% commission), **Tie** (8:1). "
"On **Tie**, Player/Banker bets **push**.\n\n"
"**Drawing:** Naturals (8 or 9) stand. Otherwise Player may draw on 05; Banker draws per standard third-card rules."
)
await ctx.send(embed=e)
async def setup(bot):
await bot.add_cog(Baccarat(bot))

439
src/cogs/blackjack.py Normal file
View File

@@ -0,0 +1,439 @@
import random, discord
from discord.ext import commands
from typing import Optional
from .. import db
# ====== Core game model (same logic you had) ======
SUITS = ['♠️', '♥️', '♦️', '♣️']
RANKS = ['A','2','3','4','5','6','7','8','9','10','J','Q','K']
CARD_VALUES = {'A':11,'2':2,'3':3,'4':4,'5':5,'6':6,'7':7,'8':8,'9':9,'10':10,'J':10,'Q':10,'K':10}
class Card:
def __init__(self, suit, rank): self.suit, self.rank = suit, rank; self.value = CARD_VALUES[rank]
def __str__(self): return f"{self.rank}{self.suit}"
class Deck:
def __init__(self):
self.cards = [Card(s, r) for s in SUITS for r in RANKS]
random.shuffle(self.cards)
def draw(self): return self.cards.pop() if self.cards else None
class Hand:
def __init__(self): self.cards = []
def add_card(self, c): self.cards.append(c)
def get_value(self):
v = sum(c.value for c in self.cards); aces = sum(1 for c in self.cards if c.rank == 'A')
while v > 21 and aces: v -= 10; aces -= 1
return v
def is_bust(self): return self.get_value() > 21
def is_blackjack(self): return len(self.cards) == 2 and self.get_value() == 21
def __str__(self): return ' '.join(str(c) for c in self.cards)
active_games = {} # uid -> BlackjackGame
class BlackjackGame:
def __init__(self, user_id: int, bet: int):
self.user_id = user_id; self.bet = bet; self.original_bet = bet
self.deck = Deck(); self.player_hand = Hand(); self.dealer_hand = Hand()
self.game_over = False; self.player_won = None; self.doubled_down = False
self.can_split = False; self.split_hands = []; self.current_split_hand = 0
self.is_split_game = False; self.split_results = []
for _ in range(2):
self.player_hand.add_card(self.deck.draw()); self.dealer_hand.add_card(self.deck.draw())
self.check_can_split()
def check_can_split(self):
if len(self.player_hand.cards) == 2:
a,b = self.player_hand.cards
if a.rank == b.rank or (a.value == 10 and b.value == 10): self.can_split = True
def can_double_down(self): return len(self.player_hand.cards) == 2 and not self.doubled_down and not self.is_split_game
def get_current_hand(self): return self.split_hands[self.current_split_hand] if self.is_split_game else self.player_hand
def hit_player(self):
h = self.get_current_hand(); h.add_card(self.deck.draw())
if h.is_bust():
if self.is_split_game:
self.split_results.append(False)
if not self.next_split_hand(): self.dealer_turn()
else:
self.game_over = True; self.player_won = False
def next_split_hand(self):
if self.is_split_game and self.current_split_hand < len(self.split_hands) - 1:
self.current_split_hand += 1; return True
return False
def double_down(self):
self.bet *= 2; self.doubled_down = True
self.hit_player()
if not self.player_hand.is_bust(): self.dealer_turn()
def split_hand(self):
if not self.can_split: return False
a,b = self.player_hand.cards
h1, h2 = Hand(), Hand()
h1.add_card(a); h1.add_card(self.deck.draw())
h2.add_card(b); h2.add_card(self.deck.draw())
self.split_hands = [h1,h2]; self.is_split_game = True; self.current_split_hand = 0
self.can_split = False; self.bet = self.original_bet
return True
def dealer_turn(self):
while self.dealer_hand.get_value() < 17: self.dealer_hand.add_card(self.deck.draw())
dv = self.dealer_hand.get_value()
if self.is_split_game:
for i,h in enumerate(self.split_hands):
if i < len(self.split_results): continue
pv = h.get_value()
if dv > 21 or pv > dv: self.split_results.append(True)
elif pv == dv: self.split_results.append(None)
else: self.split_results.append(False)
else:
pv = self.player_hand.get_value()
self.player_won = True if dv > 21 or pv > dv else (None if pv == dv else False)
self.game_over = True
def create_embed(game: BlackjackGame, reveal: bool = False) -> discord.Embed:
# (unchanged renderer from your file)
dv = game.dealer_hand.get_value()
if game.game_over:
if game.is_split_game:
w = sum(1 for r in game.split_results if r is True)
l = sum(1 for r in game.split_results if r is False)
color = discord.Color.green() if w > l else (discord.Color.red() if l > w else discord.Color.orange())
else:
color = discord.Color.green() if game.player_won is True else (discord.Color.red() if game.player_won is False else discord.Color.orange())
else: color = discord.Color.blue()
embed = discord.Embed(title="🎲 Blackjack", color=color)
# player
if game.is_split_game:
for i,h in enumerate(game.split_hands):
hv, hc = h.get_value(), str(h)
status = ""
if i < len(game.split_results) and game.split_results[i] is not None:
status = " - WON! ✅" if game.split_results[i] is True else (" - LOST 😔" if game.split_results[i] is False else " - PUSH 🤝")
elif i == game.current_split_hand and not game.game_over:
status = " - **CURRENT**"
if h.is_blackjack(): txt = f"{hc}\n**Value: {hv} - BLACKJACK! ✨**{status}"
elif h.is_bust(): txt = f"{hc}\n**Value: {hv} - BUST! 💥**{status}"
else: txt = f"{hc}\n**Value: {hv}**{status}"
embed.add_field(name=f"🧑 Your Hand #{i+1}", value=txt, inline=False)
else:
ch = game.get_current_hand(); pv, pc = ch.get_value(), str(ch)
if ch.is_blackjack(): txt = f"{pc}\n**Value: {pv} - BLACKJACK! ✨**"
elif ch.is_bust(): txt = f"{pc}\n**Value: {pv} - BUST! 💥**"
else: txt = f"{pc}\n**Value: {pv}**"
embed.add_field(name="🧑 Your Hand", value=txt, inline=False)
# dealer
if reveal or game.game_over:
dc = str(game.dealer_hand)
dt = f"{dc}\n**Value: {dv} - BUST! 💥**" if game.dealer_hand.is_bust() else f"{dc}\n**Value: {dv}**"
else:
vc = str(game.dealer_hand.cards[0])
dt = f"{vc} 🂠\n**Value: ? + {game.dealer_hand.cards[0].value}**"
embed.add_field(name="🏠 Dealer Hand", value=dt, inline=False)
# bet + result
if game.is_split_game:
embed.add_field(name="💰 Total Bet", value=f"${game.original_bet*2} (${game.original_bet} per hand)", inline=True)
elif game.doubled_down:
embed.add_field(name="💰 Bet (DOUBLED)", value=f"${game.bet}", inline=True)
else:
embed.add_field(name="💰 Bet", value=f"${game.bet}", inline=True)
if game.game_over:
if game.is_split_game:
wins = sum(1 for r in game.split_results if r is True)
losses = sum(1 for r in game.split_results if r is False)
pushes = sum(1 for r in game.split_results if r is None)
net = sum((+game.original_bet if r is True else (-game.original_bet if r is False else 0)) for r in game.split_results)
txt = f"**Split Results: {wins} wins, {losses} losses, {pushes} pushes**\n"
txt += f"**Net {'gain' if net>0 else ('loss' if net<0 else 'change')}: ${abs(net)}{'! 🎉' if net>0 else (' 😔' if net<0 else ' 🤝')}**"
else:
if game.player_won is True:
if game.player_hand.is_blackjack(): win = int(game.bet * 1.5); txt = f"**BLACKJACK! You win ${win}! 🎉**"
else: txt = f"**You win ${game.bet}! 🎉**" + (" (Doubled down!)" if game.doubled_down else "")
elif game.player_won is False:
txt = f"**You lose ${game.bet}! 😔**" + (" (Doubled down!)" if game.doubled_down else "")
else: txt = f"**Push! Bet returned. 🤝**"
embed.add_field(name="📊 Result", value=txt, inline=False)
return embed
# ====== New UI layer (Set Bet / ×2 / ½ / Deal + in-game buttons) ======
MIN_BJ_BET = 10 # simple minimum
class SetBetModal(discord.ui.Modal, title="Set Blackjack Bet"):
def __init__(self, view: "BlackjackPanel"):
super().__init__()
self.view = view
self.amount = discord.ui.TextInput(
label=f"Amount (min {MIN_BJ_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 isnt your blackjack panel.", ephemeral=True)
if self.view.game is not None and not self.view.game.game_over:
return await itx.response.send_message("You cant change bet during a hand.", 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(MIN_BJ_BET, amt)
await itx.response.defer(ephemeral=True)
if self.view.message:
await self.view.message.edit(embed=self.view.panel_embed(), view=self.view)
class BlackjackPanel(discord.ui.View):
"""
One-message blackjack:
Row 0: Set Bet, ×2, ½, Deal (deal & bet controls disabled during a hand)
Row 1: Hit, Stand, Double, Split (enabled only while a hand is active)
"""
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(MIN_BJ_BET, initial_bet)
self.game: Optional[BlackjackGame] = None
self.message: Optional[discord.Message] = None
self._busy = False
self._sync_buttons()
# ----- utils -----
def _sync_buttons(self):
# default: enable everything
for it in self.children:
it.disabled = False
hand_active = self.game is not None and not self.game.game_over
# Setup row: disable while hand active
for lbl in ("Set Bet","×2","½","Deal"):
b = self._btn(lbl)
if b: b.disabled = hand_active
# In-hand row: enable only while hand active
for lbl in ("Hit","Stand","Double Down","Split"):
b = self._btn(lbl)
if b: b.disabled = not hand_active
# More granular in-hand state
if hand_active:
h = self.game.get_current_hand()
b_hit = self._btn("Hit")
b_dd = self._btn("Double Down")
b_sp = self._btn("Split")
if b_hit and (h.is_bust() or h.get_value() == 21):
b_hit.disabled = True
if b_dd:
b_dd.disabled = not self.game.can_double_down()
if b_sp:
b_sp.disabled = not self.game.can_split
def _btn(self, label):
for c in self.children:
if isinstance(c, discord.ui.Button) and c.label == label:
return c
return None
def disable_all_items(self):
for it in self.children: it.disabled = True
async def on_timeout(self):
self.disable_all_items()
try:
if self.message:
await self.message.edit(view=self)
except: pass
# ----- rendering -----
def panel_embed(self) -> discord.Embed:
cash, _ = db.get_wallet(self.user_id)
# No hand yet → neutral panel
if self.game is None:
e = discord.Embed(title="🎲 Blackjack", color=discord.Color.blurple())
e.description = (
"Set your **bet**, then press **Deal** to start.\n"
"During a hand use **Hit / Stand / Double / Split**.\n"
f"**Current bet:** ${self.bet}\n"
)
e.add_field(name="Balance", value=f"${cash:,}", inline=True)
return e
# Hand finished → show the real result embed (already green/red/orange)
if self.game.game_over:
e = create_embed(self.game, reveal=True)
e.set_footer(text=f"Balance: ${cash:,} • Set Bet then Deal to play again")
return e
# In-progress hand → normal in-hand embed (blue)
return create_embed(self.game, reveal=False)
# ----- Setup row (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 isnt your blackjack panel.", ephemeral=True)
if self.game is not None and not self.game.game_over:
return await itx.response.send_message("You cant change bet during a hand.", ephemeral=True)
await itx.response.send_modal(SetBetModal(self))
@discord.ui.button(label="×2", style=discord.ButtonStyle.secondary, row=0)
async def bet_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.game is not None and not self.game.game_over:
return await itx.response.send_message("You cant change bet during a hand.", ephemeral=True)
self.bet = max(MIN_BJ_BET, self.bet * 2)
await itx.response.edit_message(embed=self.panel_embed(), view=self)
@discord.ui.button(label="½", style=discord.ButtonStyle.secondary, row=0)
async def bet_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.game is not None and not self.game.game_over:
return await itx.response.send_message("You cant change bet during a hand.", ephemeral=True)
self.bet = max(MIN_BJ_BET, self.bet // 2)
await itx.response.edit_message(embed=self.panel_embed(), view=self)
@discord.ui.button(label="Deal", style=discord.ButtonStyle.success, emoji="🃏", row=0)
async def deal(self, itx: discord.Interaction, _):
if itx.user.id != self.user_id: return await itx.response.send_message("Not your panel.", ephemeral=True)
if self.game is not None and not self.game.game_over:
return await itx.response.send_message("Finish the current hand first.", ephemeral=True)
if self.user_id in active_games:
return await itx.response.send_message("You already have an active blackjack hand.", ephemeral=True)
# validate funds similar to old command
cash,_ = db.get_wallet(self.user_id)
if self.bet < MIN_BJ_BET:
return await itx.response.send_message(f"Minimum bet is ${MIN_BJ_BET}.", ephemeral=True)
max_bet = cash//2 if self.bet*2 <= cash else cash
if self.bet > max_bet and self.bet != cash:
return await itx.response.send_message(f"Bet too high for doubling/splitting. Max recommended: ${max_bet}", ephemeral=True)
# start hand
game = BlackjackGame(self.user_id, self.bet)
self.game = game; active_games[self.user_id] = game
# natural blackjack resolution
if game.player_hand.is_blackjack():
if game.dealer_hand.is_blackjack():
# push
self._sync_buttons()
return await itx.response.edit_message(embed=create_embed(game, reveal=True), view=self)
else:
game.game_over=True; game.player_won=True
db.add_cash(self.user_id, int(self.bet*1.5)); db.record_blackjack(self.user_id, True)
active_games.pop(self.user_id, None)
self._sync_buttons()
return await itx.response.edit_message(embed=create_embed(game, reveal=True), view=self)
# regular start
self._sync_buttons()
await itx.response.edit_message(embed=self.panel_embed(), view=self)
# ----- In-hand row (row 1) -----
@discord.ui.button(label="Hit", style=discord.ButtonStyle.primary, row=1)
async def hit(self, itx: discord.Interaction, _):
if itx.user.id != self.user_id: return await itx.response.send_message("Not your hand.", ephemeral=True)
if self.game is None or self.game.game_over: return
self.game.hit_player()
if self.game.game_over:
# bust
db.add_cash(self.user_id, -self.game.bet); db.record_blackjack(self.user_id, False)
active_games.pop(self.user_id, None)
self._sync_buttons()
await itx.response.edit_message(embed=self.panel_embed(), view=self)
@discord.ui.button(label="Stand", style=discord.ButtonStyle.secondary, row=1)
async def stand(self, itx: discord.Interaction, _):
if itx.user.id != self.user_id: return await itx.response.send_message("Not your hand.", ephemeral=True)
if self.game is None or self.game.game_over: return
if self.game.is_split_game:
if not self.game.next_split_hand():
self.game.dealer_turn()
# else fall through to render below
else:
self.game.dealer_turn()
if self.game.game_over:
# settle
if self.game.is_split_game:
net = 0; wins = 0
for r in self.game.split_results:
if r is True: net += self.game.original_bet; wins += 1
elif r is False: net -= self.game.original_bet
if net: db.add_cash(self.user_id, net)
for i in range(2): db.record_blackjack(self.user_id, i < wins)
else:
if self.game.player_won is True:
if self.game.player_hand.is_blackjack(): db.add_cash(self.user_id, int(self.game.bet*1.5))
else: db.add_cash(self.user_id, self.game.bet)
elif self.game.player_won is False:
db.add_cash(self.user_id, -self.game.bet)
db.record_blackjack(self.user_id, self.game.player_won is True)
active_games.pop(self.user_id, None)
self._sync_buttons()
await itx.response.edit_message(embed=self.panel_embed(), view=self)
@discord.ui.button(label="Double Down", style=discord.ButtonStyle.success, row=1)
async def double_down(self, itx: discord.Interaction, _):
if itx.user.id != self.user_id: return await itx.response.send_message("Not your hand.", ephemeral=True)
if self.game is None or self.game.game_over: return
if not self.game.can_double_down():
return await itx.response.send_message("❌ Cannot double down now!", ephemeral=True)
cash,_ = db.get_wallet(self.user_id)
if cash < self.game.bet:
return await itx.response.send_message(f"❌ Not enough cash to double down! Need ${self.game.bet} more.", ephemeral=True)
self.game.double_down()
if self.game.player_won is True: db.add_cash(self.user_id, self.game.bet)
elif self.game.player_won is False: db.add_cash(self.user_id, -self.game.bet)
db.record_blackjack(self.user_id, self.game.player_won is True)
active_games.pop(self.user_id, None)
self._sync_buttons()
await itx.response.edit_message(embed=self.panel_embed(), view=self)
@discord.ui.button(label="Split", style=discord.ButtonStyle.danger, row=1)
async def split(self, itx: discord.Interaction, _):
if itx.user.id != self.user_id: return await itx.response.send_message("Not your hand.", ephemeral=True)
if self.game is None or self.game.game_over: return
if not self.game.can_split:
return await itx.response.send_message("❌ Cannot split this hand!", ephemeral=True)
cash,_ = db.get_wallet(self.user_id)
if cash < self.game.bet:
return await itx.response.send_message(f"❌ Not enough cash to split! Need ${self.game.bet} more.", ephemeral=True)
self.game.split_hand()
self._sync_buttons()
await itx.response.edit_message(embed=self.panel_embed(), view=self)
class BlackjackCog(commands.Cog):
def __init__(self, bot): self.bot = bot
@commands.command(name="blackjack", aliases=["bj"])
async def blackjack(self, ctx, bet: int = None):
"""
Open the Blackjack panel.
Tip: use the buttons to Set Bet / ×2 / ½, then Deal.
"""
uid = ctx.author.id
if uid in active_games:
return await ctx.send("❌ You already have an active blackjack hand. Finish it first.")
init_bet = 50 if bet is None else max(MIN_BJ_BET, bet)
view = BlackjackPanel(uid, initial_bet=init_bet, timeout=180)
msg = await ctx.send(embed=view.panel_embed(), view=view)
view.message = msg
async def setup(bot):
await bot.add_cog(BlackjackCog(bot))

196
src/cogs/coinflip.py Normal file
View File

@@ -0,0 +1,196 @@
# src/cogs/coinflip.py
# Coin Flip with Towers-style "Set Bet" modal and a minimal 2-row UI.
# Buttons: [Set Bet, ×2, ½] and [Heads, Tails]
#
# Commands:
# !coin (aliases: !coinflip, !cf, !flip)
# !rules_coin (optional)
import random
import discord
from discord.ext import commands
from typing import Optional, Dict
from .. import db
from ..utils.constants import (
COINFLIP_MIN_CHIP, # minimum allowed chip value
COINFLIP_MIN_BET, # keep for limits consistency (we enforce MIN_CHIP)
COINFLIP_MAX_BET, # maximum per flip
)
COIN_EMOJI = "🪙"
HEADS_TXT = "Heads"
TAILS_TXT = "Tails"
_active_coin_sessions: Dict[int, "CoinFlipView"] = {}
def flip_coin() -> str:
return HEADS_TXT if random.random() < 0.5 else TAILS_TXT
class SetBetModal(discord.ui.Modal, title="Set Coin Flip Bet"):
def __init__(self, view: "CoinFlipView"):
super().__init__()
self.view = view
self.amount = discord.ui.TextInput(
label=f"Amount (min {COINFLIP_MIN_CHIP}, max {COINFLIP_MAX_BET})",
placeholder=str(self.view.chip or COINFLIP_MIN_CHIP),
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 isnt your coin session!", 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)
amt = max(COINFLIP_MIN_CHIP, min(COINFLIP_MAX_BET, amt))
self.view.chip = amt
await itx.response.defer(ephemeral=True)
if self.view.message:
await self.view.message.edit(embed=self.view.embed(), view=self.view)
class CoinFlipView(discord.ui.View):
def __init__(self, user_id: int, *, timeout: int = 120):
super().__init__(timeout=timeout)
self.user_id = user_id
self.chip = max(COINFLIP_MIN_CHIP, 100)
self.last_choice: Optional[str] = None
self.last_result: Optional[str] = None
self.message: Optional[discord.Message] = None
self._busy = False
# ---------- lifecycle ----------
def disable_all_items(self):
for item in self.children:
item.disabled = True
async def on_timeout(self):
self.disable_all_items()
try:
if self.message:
await self.message.edit(view=self)
finally:
_active_coin_sessions.pop(self.user_id, None)
# ---------- rendering ----------
def embed(self) -> discord.Embed:
e = discord.Embed(title="🪙 Coin Flip", color=discord.Color.blurple())
if self.last_result is not None:
outcome = "✅ WIN" if self.last_choice == self.last_result else "❌ LOSS"
e.description = f"{COIN_EMOJI} **{self.last_result}** • You picked **{self.last_choice}** → {outcome}"
else:
e.description = "Pick **Heads** or **Tails**, set your **Bet**, and flip!"
cash, _ = db.get_wallet(self.user_id)
e.add_field(name="Bet per flip", value=f"${self.chip}", inline=True)
e.add_field(name="Balance", value=f"${cash:,}", inline=True)
e.set_footer(text="Flip as many times as you like. View times out after ~2 minutes.")
return e
# ---------- guards ----------
async def _ensure_owner(self, itx: discord.Interaction):
if itx.user.id != self.user_id:
raise await itx.response.send_message("This isnt your coin session!", ephemeral=True)
# ---------- actions ----------
async def _play(self, itx: discord.Interaction, choice: str):
await self._ensure_owner(itx)
if self._busy:
return await itx.response.send_message("Hang on—resolving the last flip…", ephemeral=True)
if self.chip < COINFLIP_MIN_CHIP:
return await itx.response.send_message(f"Set a bet first (min ${COINFLIP_MIN_CHIP}).", ephemeral=True)
if self.chip > COINFLIP_MAX_BET:
return await itx.response.send_message(f"Max per-flip bet is ${COINFLIP_MAX_BET}.", ephemeral=True)
cash, _ = db.get_wallet(self.user_id)
if cash < self.chip:
return await itx.response.send_message("Not enough cash for that flip.", ephemeral=True)
self._busy = True
try:
# take the bet
db.add_cash(self.user_id, -self.chip)
# flip & resolve
self.last_choice = choice
self.last_result = flip_coin()
won = (self.last_choice == self.last_result)
return_amount = self.chip * 2 if won else 0 # 1:1 payout → return chip*2 on win
if return_amount:
db.add_cash(self.user_id, return_amount)
# record stats (kept even if not shown on balance)
try:
db.record_coinflip(self.user_id, bet=self.chip, return_amount=return_amount, won=won)
except Exception:
pass
await itx.response.edit_message(embed=self.embed(), view=self)
finally:
self._busy = False
# ---------- controls ----------
# Row 0 — bet controls
@discord.ui.button(label="Set Bet", style=discord.ButtonStyle.secondary, row=0)
async def set_bet(self, itx: discord.Interaction, _):
await self._ensure_owner(itx)
await itx.response.send_modal(SetBetModal(self))
@discord.ui.button(label="×2", style=discord.ButtonStyle.secondary, row=0)
async def bet_x2(self, itx: discord.Interaction, _):
await self._ensure_owner(itx)
self.chip = max(COINFLIP_MIN_CHIP, min(COINFLIP_MAX_BET, self.chip * 2))
await itx.response.edit_message(embed=self.embed(), view=self)
@discord.ui.button(label="½", style=discord.ButtonStyle.secondary, row=0)
async def bet_half(self, itx: discord.Interaction, _):
await self._ensure_owner(itx)
self.chip = max(COINFLIP_MIN_CHIP, self.chip // 2)
await itx.response.edit_message(embed=self.embed(), view=self)
# Row 1 — play buttons
@discord.ui.button(label="Heads", style=discord.ButtonStyle.success, emoji="🙂", row=1)
async def pick_heads(self, itx: discord.Interaction, _):
await self._play(itx, HEADS_TXT)
@discord.ui.button(label="Tails", style=discord.ButtonStyle.primary, emoji="🌀", row=1)
async def pick_tails(self, itx: discord.Interaction, _):
await self._play(itx, TAILS_TXT)
class CoinFlip(commands.Cog):
def __init__(self, bot): self.bot = bot
@commands.command(name="coin", aliases=["coinflip","cf","flip"])
async def coin(self, ctx: commands.Context):
uid = ctx.author.id
if uid in _active_coin_sessions:
return await ctx.send("❌ You already have an active coin session. Finish it or wait for it to time out.")
view = CoinFlipView(uid, timeout=120)
msg = await ctx.send(embed=view.embed(), view=view)
view.message = msg
_active_coin_sessions[uid] = view
@commands.command(name="rules_coin")
async def rules_coin(self, ctx: commands.Context):
e = discord.Embed(title="🪙 Coin Flip — Rules", color=discord.Color.blurple())
e.description = (
"Pick **Heads** or **Tails**, set your **Bet**, and flip.\n\n"
"**Payout:** 1:1 (win doubles your bet; loss loses your bet)\n"
f"**Limits:** min bet ${COINFLIP_MIN_CHIP} • per flip ${COINFLIP_MIN_BET}${COINFLIP_MAX_BET}\n"
"_Tip: Use ×2 / ½ to adjust quickly. The panel times out after a bit of inactivity._"
)
await ctx.send(embed=e)
async def setup(bot):
await bot.add_cog(CoinFlip(bot))

325
src/cogs/economy.py Normal file
View File

@@ -0,0 +1,325 @@
import discord
from discord.ext import commands
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) ----------------
class RulesView(discord.ui.View):
def __init__(self):
super().__init__(timeout=60)
self.add_item(RulesSelect())
class RulesSelect(discord.ui.Select):
def __init__(self):
options = [
discord.SelectOption(label="Blackjack", emoji="🃏", description="How to play Blackjack"),
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="Coin Flip", emoji="🪙", description="How to play Coin Flip"),
discord.SelectOption(label="Towers", emoji="🐉", description="How to play Towers"),
discord.SelectOption(label="Baccarat", emoji="🎴", description="How to play Baccarat"),
]
super().__init__(placeholder="Choose a game…", options=options, min_values=1, max_values=1)
async def callback(self, interaction: discord.Interaction):
choice = self.values[0]
if choice == "Blackjack":
embed = rules_blackjack_embed()
elif choice == "Slots":
embed = rules_slots_embed()
elif choice == "Roulette (Mini)":
embed = rules_roulette_mini_embed()
elif choice == "Towers":
embed = rules_towers_embed()
elif choice == "Baccarat":
embed = rules_baccarat_embed()
else: # Coin Flip
embed = rules_coin_embed()
await interaction.response.edit_message(embed=embed, view=self.view)
def rules_baccarat_embed():
e = discord.Embed(title="🎴 Baccarat — Rules", color=discord.Color.dark_green())
e.description = (
"**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"
"Naturals (8/9) stand; otherwise Player/Banker draw by standard third-card rules."
)
return e
def rules_towers_embed():
e = discord.Embed(title="🐉 Towers — Rules", color=discord.Color.dark_teal())
e.description = (
"Nine tiers high. Pick one tile per row.\n"
"• **Easy:** 4 slots / 1 bad (3 safe)\n"
"• **Medium:** 3 / 1\n"
"• **Hard:** 2 / 1\n"
"• **Expert:** 3 / 2\n"
"• **Master:** 4 / 3\n\n"
"You pay your bet once on the first pick. Each safe pick multiplies your cashout; "
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."
)
return e
def rules_coin_embed():
e = discord.Embed(title="🪙 Coin Flip — Rules", color=discord.Color.blurple())
e.description = (
"Pick **Heads** or **Tails**, set your **Chip**, and flip.\n"
"**Payout:** 1:1 • "
f"**Limits:** min chip ${COINFLIP_MIN_CHIP}, per flip ${COINFLIP_MIN_BET}${COINFLIP_MAX_BET}"
)
return e
def rules_blackjack_embed():
e = discord.Embed(title="🃏 Blackjack — Rules", color=discord.Color.blue())
e.description = (
"**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"
"**Actions:** Hit, Stand, Double Down (first two cards), Split (matching ranks/10-values).\n"
"Dealer hits to 16, stands on 17.\n\n"
"**Payouts:** Win=1:1, Blackjack=3:2, Push=bet returned."
)
return e
def rules_slots_embed():
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```"
e = discord.Embed(title="🎰 Fruit Slots — Rules", color=discord.Color.purple())
e.description = (
"**Board:** 3×3 grid. Wins on rows, columns, and diagonals.\n"
f"**Bet:** `!slots <bet>` (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"
"• 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"**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)."
)
e.add_field(name="Payout Multipliers", value=table, inline=False)
return e
def rules_roulette_mini_embed():
e = discord.Embed(title="🎯 Mini Roulette — Rules", color=discord.Color.gold())
e.description = (
"**Wheel:** 19 pockets (018). Casino edge ≈ 1/19.\n\n"
"**Outside Bets:** Red/Black 1:1 • Even/Odd 1:1 (0 loses) • Low/High 1:1 • "
"Column (6 nums) 2:1 • Sixes (16 / 712 / 1318) 2:1\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**."
)
return e
# ---------------- Casino menu ----------------
class CasinoView(discord.ui.View):
"""Buttons to launch any game for the invoking user."""
def __init__(self, cog: "Economy", ctx: commands.Context):
super().__init__(timeout=120)
self.cog = cog
self.ctx = ctx
self.user_id = ctx.author.id
async def interaction_check(self, interaction: discord.Interaction) -> bool:
if interaction.user.id != self.user_id:
await interaction.response.send_message("This menu belongs to someone else.", ephemeral=True)
return False
return True
async def _launch(self, interaction: discord.Interaction, command_name: str):
# Acknowledge the click fast so Discord doesn't show "interaction failed"
try:
await interaction.response.defer(thinking=False)
except Exception:
pass
# Delete the casino menu message to avoid clutter
try:
await interaction.message.delete()
except Exception:
# Fallback: disable the view & mark as launching
for item in self.children:
item.disabled = True
try:
await interaction.message.edit(content="🎮 Launching…", view=self)
except Exception:
pass
# Now invoke the actual game command
cmd = self.cog.bot.get_command(command_name)
if cmd is None:
return await self.ctx.send(f"⚠️ Command `{command_name}` not found.")
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="Slots", style=discord.ButtonStyle.primary, emoji="🎰", row=0)
async def slots(self, itx: discord.Interaction, _): await self._launch(itx, "slots")
@discord.ui.button(label="Mini Roulette", style=discord.ButtonStyle.secondary, emoji="🎯", row=1)
async def roulette(self, itx: discord.Interaction, _): await self._launch(itx, "roulette")
@discord.ui.button(label="Coin Flip", style=discord.ButtonStyle.secondary, emoji="🪙", row=1)
async def coin(self, itx: discord.Interaction, _): await self._launch(itx, "coin")
@discord.ui.button(label="Towers", style=discord.ButtonStyle.secondary, emoji="🐉", row=2)
async def towers(self, itx: discord.Interaction, _): await self._launch(itx, "towers")
@discord.ui.button(label="Baccarat", style=discord.ButtonStyle.secondary, emoji="🎴", row=2)
async def baccarat(self, itx: discord.Interaction, _): await self._launch(itx, "baccarat")
class Economy(commands.Cog):
def __init__(self, bot):
self.bot = bot
async def resolve_name(self, ctx, user_id: int) -> str:
if ctx.guild:
m = ctx.guild.get_member(user_id)
if m:
return m.display_name
try:
u = await self.bot.fetch_user(user_id)
return getattr(u, "global_name", None) or u.name
except Exception:
return f"User {user_id}"
@commands.command(name="casino")
async def casino(self, ctx: commands.Context):
"""Open the casino launcher with game buttons."""
cash, spins = db.get_wallet(ctx.author.id)
e = discord.Embed(
title="🏟️ Casino",
description=(
"Pick a game below to start playing.\n"
"Use **!rules** for how-to-play and payouts."
),
color=discord.Color.blurple()
)
e.add_field(name="Your Balance", value=f"${cash:,} • 🎟️ {spins} free spins", inline=False)
await ctx.send(embed=e, view=CasinoView(self, ctx))
@commands.command(name="balance", aliases=["bal", "cash", "stats"])
async def balance(self, ctx, user: discord.Member = None):
target = user or ctx.author
s = db.get_full_stats(target.id)
def pct(n: int, d: int) -> str:
return f"{(n/d*100):.1f}%" if d else "0.0%"
embed = discord.Embed(
title=f"💰 {target.display_name}'s Stats",
color=discord.Color.gold()
)
if target.avatar:
embed.set_thumbnail(url=target.avatar.url)
# Topline economy
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="Overall W/L", value=f"{s['games_won']}/{s['games_played']} ({pct(s['games_won'], s['games_played'])})", 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="🎯 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="🐉 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)
await ctx.send(embed=embed)
@commands.command(name="daily")
async def daily(self, ctx):
user_id = ctx.author.id
can, secs_left = db.daily_cooldown(user_id, DAILY_COOLDOWN_HOURS)
if not can:
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
db.claim_daily(user_id, DAILY_CASH, DAILY_FREE_SPINS)
stats = db.get_full_stats(user_id)
embed = discord.Embed(
title="🎁 Daily Bonus!",
description=(f"You received **${DAILY_CASH}** and **{DAILY_FREE_SPINS} free spins**!\n"
f"New balance: **${stats['cash']}**, Free spins: **{stats['free_spins']}**"),
color=discord.Color.green()
)
await ctx.send(embed=embed)
@commands.command(name="leaderboard", aliases=["lb"])
async def leaderboard(self, ctx):
rows = db.top_cash(10)
if not rows: return await ctx.send("No players found!")
lines = []
for i, (uid, cash) in enumerate(rows, 1):
name = await self.resolve_name(ctx, uid)
medal = "🥇" if i == 1 else "🥈" if i == 2 else "🥉" if i == 3 else f"{i}."
lines.append(f"{medal} **{name}** - ${cash}")
embed = discord.Embed(title="🏆 Cash Leaderboard", description="\n".join(lines), color=discord.Color.gold())
await ctx.send(embed=embed)
@commands.command(name="status")
async def status(self, ctx):
try:
count = db.user_counts()
db_status = f"✅ Connected ({count} users)"
except Exception as e:
db_status = f"❌ Error: {e}"
embed = discord.Embed(title="🤖 Bot Status", color=discord.Color.green())
embed.add_field(name="Database", value=db_status, inline=False)
embed.add_field(name="Servers", value=len(self.bot.guilds), inline=True)
await ctx.send(embed=embed)
@commands.command(name="rules")
async def rules(self, ctx):
embed = discord.Embed(title="📜 Game Rules", description="Choose a game from the menu below.", color=discord.Color.blurple())
await ctx.send(embed=embed, view=RulesView())
@commands.command(name="topup", aliases=["boost","faucet"])
async def topup(self, ctx):
uid = ctx.author.id
can, secs_left = db.can_claim_topup(uid, TOPUP_COOLDOWN_MINUTES)
if not can:
mins = secs_left // 60; secs = secs_left % 60
return await ctx.send(f"⏳ You can claim again in **{mins}m {secs}s**.")
db.claim_topup(uid, TOPUP_AMOUNT)
stats = db.get_full_stats(uid)
embed = discord.Embed(
title="⚡ Top-up Claimed!",
description=f"You received **${TOPUP_AMOUNT}**.\nNew balance: **${stats['cash']}**",
color=discord.Color.green()
)
await ctx.send(embed=embed)
@commands.command(name="tip", aliases=["pay", "gift", "give"])
async def tip(self, ctx: commands.Context, member: discord.Member, amount: str, *, note: str | None = None):
if member.bot: return await ctx.send("🤖 You cant tip bots.")
if member.id == ctx.author.id: return await ctx.send("🪞 You cant tip yourself.")
try: amt = int("".join(ch for ch in amount if ch.isdigit()))
except Exception: return await ctx.send("❌ Please provide a valid amount, e.g. `!tip @user 500`.")
if amt <= 0: return await ctx.send("❌ Tip amount must be at least $1.")
if amt > 1_000_000: return await ctx.send("🛑 Thats a bit much. Max tip is $1,000,000 by default.")
ok, err, from_cash, to_cash = db.transfer_cash(ctx.author.id, member.id, amt)
if not ok:
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 cant tip yourself.")
return await ctx.send("❌ Couldnt complete the tip. Try again in a moment.")
sender = await self.resolve_name(ctx, ctx.author.id)
recv = await self.resolve_name(ctx, member.id)
desc = f"**{sender}** → **{recv}**\nAmount: **${amt:,}**"
if note: desc += f"\n{note}"
e = discord.Embed(title="🎁 Tip sent!", description=desc, color=discord.Color.green())
e.add_field(name="Your new balance", value=f"${from_cash:,}", inline=True)
e.add_field(name=f"{recv}'s new balance", value=f"${to_cash:,}", inline=True)
await ctx.send(embed=e)
async def setup(bot):
await bot.add_cog(Economy(bot))

590
src/cogs/roulette.py Normal file
View File

@@ -0,0 +1,590 @@
# src/cogs/roulette.py
# Mini Roulette (018) with a single ephemeral "Bet Builder" panel.
# - Outside/Inside flows live in ONE popup; each step edits the same ephemeral message.
# - Back / Close buttons on every picker.
# - Clear Slip also resets chip to $0.
# - Requires:
# from ..utils.constants import (ROULETTE_NUMBERS, ROULETTE_RED, ROULETTE_BLACK,
# ROULETTE_MIN_CHIP, ROULETTE_MIN_BET, ROULETTE_MAX_BET)
# from .. import db
import random, discord
from typing import List, Tuple, Dict, Any, Optional
from discord.ext import commands
from ..utils.constants import (
ROULETTE_NUMBERS, ROULETTE_RED, ROULETTE_BLACK,
ROULETTE_MIN_CHIP, ROULETTE_MIN_BET, ROULETTE_MAX_BET,
)
from .. import db
P = len(ROULETTE_NUMBERS)
assert P == 19, "Mini Roulette expects pockets 0..18"
# ===== helpers =====
def spin() -> int:
return random.choice(ROULETTE_NUMBERS)
def color_of(n: int) -> str:
if n == 0: return "green"
return "red" if n in ROULETTE_RED else "black"
def column_of(n: int) -> Optional[int]:
if n == 0: return None
r = n % 3
return 1 if r == 1 else 2 if r == 2 else 3
def street_rows() -> List[Tuple[int,int,int]]:
rows = [(a, a+1, a+2) for a in range(1, 19, 3)] # 13,46,…,1618
rows += [(0,1,2), (0,2,3)]
return rows
def sixlines_all() -> List[Tuple[int, int, int, int, int, int]]:
return [(a,a+1,a+2,a+3,a+4,a+5) for a in (1,4,7,10,13)]
def six_group_of(n: int) -> Optional[int]:
if n == 0: return None
if 1 <= n <= 6: return 1
if 7 <= n <= 12: return 2
if 13 <= n <= 18: return 3
return None
def payout_for(kind: str) -> int:
# Pay (18/K - 1):1 → house edge ≈ 1/19 across bet types
ksize = {
"straight": 1, "split": 2, "street": 3, "sixline": 6,
"column": 6, "sixes": 6, "redblack": 9, "evenodd": 9, "lowhigh": 9
}[kind]
return int((18 / ksize) - 1)
# ===== Main slip view =====
class MiniRouletteView(discord.ui.View):
def __init__(self, bot, user_id: int, *, timeout: int = 120):
super().__init__(timeout=timeout)
self.bot = bot
self.user_id = user_id
self.chip = max(ROULETTE_MIN_CHIP, 100)
self.bets: List[Dict[str,Any]] = []
self.message: Optional[discord.Message] = None
self.last_result: Optional[int] = None
self._busy = False
# --- helpers ---
def total_bet(self) -> int:
return sum(b["amount"] for b in self.bets)
def embed_slip(self) -> discord.Embed:
slip = "\n".join(f"{b['label']}" for b in self.bets) if self.bets else "_(no bets yet)_"
e = discord.Embed(title="🎯 Mini Roulette (018)", color=discord.Color.dark_gold())
if self.last_result is not None:
n = self.last_result
c = color_of(n).title()
col = column_of(n) or ""
sx = six_group_of(n) or ""
e.description = f"Last: **{n}** ({c}) • Column **{col}** • Sixes **{sx}**"
e.add_field(name="Your Bet Slip", value=slip, inline=False)
hint = " (set one ⬆️)" if self.chip == 0 else ""
e.add_field(name="Totals", value=f"Spin total: **${self.total_bet()}** • Chip: **${self.chip}**{hint}", inline=False)
e.set_footer(text="Add bets below, then Spin. View times out after ~2 minutes.")
return e
def disable_all_items(self):
for item in self.children:
item.disabled = True
async def on_timeout(self):
self.disable_all_items()
try:
if self.message:
await self.message.edit(view=self)
except Exception:
pass
# --- chip controls (row 0; ≤5 per row) ---
@discord.ui.button(label="+10", style=discord.ButtonStyle.secondary, row=0)
async def chip_p10(self, itx: discord.Interaction, _):
await self._chip_delta(itx, 10)
@discord.ui.button(label="+50", style=discord.ButtonStyle.secondary, row=0)
async def chip_p50(self, itx: discord.Interaction, _):
await self._chip_delta(itx, 50)
@discord.ui.button(label="+100", style=discord.ButtonStyle.secondary, row=0)
async def chip_p100(self, itx: discord.Interaction, _):
await self._chip_delta(itx, 100)
@discord.ui.button(label="×2", style=discord.ButtonStyle.secondary, row=0)
async def chip_x2(self, itx: discord.Interaction, _):
await self._chip_mul(itx, 2)
@discord.ui.button(label="½", style=discord.ButtonStyle.secondary, row=0)
async def chip_half(self, itx: discord.Interaction, _):
await self._chip_div(itx, 2)
@discord.ui.button(label="MAX", style=discord.ButtonStyle.secondary, row=1)
async def chip_max(self, itx: discord.Interaction, _):
if itx.user.id != self.user_id:
return await itx.response.send_message("This isnt your roulette session!", ephemeral=True)
cash, _ = db.get_wallet(self.user_id)
self.chip = max(ROULETTE_MIN_CHIP, min(ROULETTE_MAX_BET, cash))
await itx.response.edit_message(embed=self.embed_slip(), view=self)
async def _chip_delta(self, itx: discord.Interaction, d: int):
if itx.user.id != self.user_id:
return await itx.response.send_message("This isnt your roulette session!", ephemeral=True)
self.chip = max(ROULETTE_MIN_CHIP, min(ROULETTE_MAX_BET, self.chip + d))
await itx.response.edit_message(embed=self.embed_slip(), view=self)
async def _chip_mul(self, itx: discord.Interaction, m: float):
if itx.user.id != self.user_id:
return await itx.response.send_message("This isnt your roulette session!", ephemeral=True)
self.chip = max(ROULETTE_MIN_CHIP, min(ROULETTE_MAX_BET, int(self.chip * m)))
await itx.response.edit_message(embed=self.embed_slip(), view=self)
async def _chip_div(self, itx: discord.Interaction, d: float):
if itx.user.id != self.user_id:
return await itx.response.send_message("This isnt your roulette session!", ephemeral=True)
self.chip = max(ROULETTE_MIN_CHIP, int(self.chip / d))
await itx.response.edit_message(embed=self.embed_slip(), view=self)
# --- bet building entry (row 1) ---
@discord.ui.button(label="Add Outside / Inside", style=discord.ButtonStyle.secondary, emoji="🧾", row=1)
async def open_builder(self, itx: discord.Interaction, _):
if itx.user.id != self.user_id:
return await itx.response.send_message("This isnt your roulette session!", ephemeral=True)
if self.chip < ROULETTE_MIN_CHIP:
return await itx.response.send_message(
f"Set a chip amount first (min ${ROULETTE_MIN_CHIP}) using the chip buttons.", ephemeral=True
)
await itx.response.send_message("Bet Builder — choose an option:", ephemeral=True, view=BetPanelRoot(self))
@discord.ui.button(label="Clear Slip", style=discord.ButtonStyle.danger, emoji="🧹", row=1)
async def clear(self, itx: discord.Interaction, _):
if itx.user.id != self.user_id:
return await itx.response.send_message("This isnt your roulette session!", ephemeral=True)
self.bets.clear()
self.chip = 0 # reset chip as requested
await itx.response.edit_message(embed=self.embed_slip(), view=self)
# --- spin & resolve (row 2) ---
@discord.ui.button(label="Spin", style=discord.ButtonStyle.success, emoji="▶️", row=2)
async def spin_btn(self, itx: discord.Interaction, _):
if itx.user.id != self.user_id:
return await itx.response.send_message("This isnt your roulette session!", ephemeral=True)
if self._busy:
return await itx.response.send_message("Hold on—resolving the previous spin…", ephemeral=True)
if not self.bets:
return await itx.response.send_message("Add at least one bet first.", ephemeral=True)
total = self.total_bet()
if total < ROULETTE_MIN_BET:
return await itx.response.send_message(f"Min spin total is ${ROULETTE_MIN_BET}.", ephemeral=True)
if total > ROULETTE_MAX_BET:
return await itx.response.send_message(f"Max spin total is ${ROULETTE_MAX_BET}.", ephemeral=True)
cash, _ = db.get_wallet(self.user_id)
if cash < total:
return await itx.response.send_message(f"Not enough cash for **${total}**.", ephemeral=True)
self._busy = True
try:
db.add_cash(self.user_id, -total)
n = spin()
self.last_result = n
c = color_of(n)
col = column_of(n)
sx = six_group_of(n)
def bet_wins(b: Dict[str,Any]) -> bool:
kind, t = b["kind"], b["targets"]
if kind in ("straight","split","street","sixline"):
return n in t
if kind == "column": return col == int(t)
if kind == "sixes": return sx == int(t)
if kind == "redblack": return (n != 0) and (c == t)
if kind == "evenodd": return (n != 0) and ((n % 2 == 0) == (t == "even"))
if kind == "lowhigh": return (n != 0) and ((1 <= n <= 9) if t == "low" else (10 <= n <= 18))
return False
total_return = 0
lines = []
for b in self.bets:
if bet_wins(b):
win_amt = b["amount"] * b["payout"]
total_return += win_amt + b["amount"]
lines.append(f"{b['label']} → +${win_amt}")
else:
lines.append(f"{b['label']}")
if total_return:
db.add_cash(self.user_id, total_return)
try:
db.record_roulette(self.user_id, total_bet=total, total_return=total_return)
except Exception:
pass
cash2, _ = db.get_wallet(self.user_id)
head = f"**Result:** {n} ({c.title()})"
if col: head += f" • Column {col}"
if sx: head += f" • Sixes {sx}"
net = total_return - total
summary = f"**Bet:** ${total} • **Return:** ${total_return} • **Net:** {'+' if net>=0 else ''}${net}\n**Balance:** ${cash2}"
e = discord.Embed(title="🎯 Mini Roulette (018)", color=discord.Color.gold(), description=head)
e.add_field(name="Bets", value="\n".join(lines), inline=False)
e.add_field(name="Summary", value=summary, inline=False)
e.set_footer(text="Spin Again to reuse your slip, or Clear to start over.")
await itx.response.edit_message(embed=e, view=self)
finally:
self._busy = False
@discord.ui.button(label="Spin Again", style=discord.ButtonStyle.primary, emoji="🔁", row=2)
async def spin_again(self, itx: discord.Interaction, btn):
await self.spin_btn.callback(self, itx, btn)
# ===== Single Ephemeral "Bet Builder" =====
class BetPanelRoot(discord.ui.View):
"""Top-level builder: one ephemeral panel that everything else edits."""
def __init__(self, parent: MiniRouletteView):
super().__init__(timeout=60)
self.parent = parent
@discord.ui.button(label="Add Outside Bet", style=discord.ButtonStyle.primary, emoji="🟦", row=0)
async def go_outside(self, itx: discord.Interaction, _):
await itx.response.edit_message(content="Outside bet — pick a target:", view=OutsidePicker(self.parent))
@discord.ui.button(label="Add Inside Bet", style=discord.ButtonStyle.primary, emoji="🟥", row=0)
async def go_inside(self, itx: discord.Interaction, _):
await itx.response.edit_message(content="Inside bet — choose type:", view=InsideKindPicker(self.parent))
@discord.ui.button(label="Close", style=discord.ButtonStyle.secondary, emoji="✖️", row=0)
async def close_panel(self, itx: discord.Interaction, _):
await itx.response.edit_message(content="Bet Builder closed.", view=None)
# ---- Outside flow (single select) ----
class OutsidePicker(discord.ui.View):
def __init__(self, parent: MiniRouletteView):
super().__init__(timeout=60); self.parent = parent
opts = [
discord.SelectOption(label="Red", value="redblack:red", emoji="🔴"),
discord.SelectOption(label="Black", value="redblack:black", emoji=""),
discord.SelectOption(label="Even", value="evenodd:even"),
discord.SelectOption(label="Odd", value="evenodd:odd"),
discord.SelectOption(label="Low (19)", value="lowhigh:low"),
discord.SelectOption(label="High (1018)", value="lowhigh:high"),
discord.SelectOption(label="Column 1", value="column:1"),
discord.SelectOption(label="Column 2", value="column:2"),
discord.SelectOption(label="Column 3", value="column:3"),
discord.SelectOption(label="Sixes 16", value="sixes:1"),
discord.SelectOption(label="Sixes 712", value="sixes:2"),
discord.SelectOption(label="Sixes 1318", value="sixes:3"),
]
sel = discord.ui.Select(placeholder="Choose…", min_values=1, max_values=1, options=opts)
self.add_item(sel)
async def on_pick(itx: discord.Interaction):
kind, target = sel.values[0].split(":")
label = {
"redblack:red": "Red", "redblack:black": "Black",
"evenodd:even": "Even", "evenodd:odd": "Odd",
"lowhigh:low": "Low (19)", "lowhigh:high": "High (1018)",
"column:1": "Column 1", "column:2": "Column 2", "column:3": "Column 3",
"sixes:1": "Sixes 16", "sixes:2": "Sixes 712", "sixes:3": "Sixes 1318",
}[sel.values[0]]
bet = {"kind": kind, "targets": target, "amount": self.parent.chip,
"label": f"{label} ${self.parent.chip}", "payout": payout_for(kind)}
self.parent.bets.append(bet)
await itx.response.edit_message(content=f"✅ Added: {label}\n\nBet Builder — choose another option:",
view=BetPanelRoot(self.parent))
try:
await self.parent.message.edit(embed=self.parent.embed_slip(), view=self.parent)
except Exception:
pass
sel.callback = on_pick
@discord.ui.button(label="Back", style=discord.ButtonStyle.secondary, row=1)
async def back(self, itx: discord.Interaction, _):
await itx.response.edit_message(content="Bet Builder — choose an option:", view=BetPanelRoot(self.parent))
@discord.ui.button(label="Close", style=discord.ButtonStyle.secondary, emoji="✖️", row=1)
async def close(self, itx: discord.Interaction, _):
await itx.response.edit_message(content="Bet Builder closed.", view=None)
# ---- Inside flow: choose type ----
class InsideKindPicker(discord.ui.View):
def __init__(self, parent: MiniRouletteView):
super().__init__(timeout=60); self.parent = parent
sel = discord.ui.Select(
placeholder="Inside type…", min_values=1, max_values=1,
options=[
discord.SelectOption(label="Straight (single number)", value="straight"),
discord.SelectOption(label="Split (two adjacent)", value="split"),
discord.SelectOption(label="Street (row of 3)", value="street"),
discord.SelectOption(label="Six-line (two rows)", value="sixline"),
]
)
self.add_item(sel)
async def on_pick(itx: discord.Interaction):
k = sel.values[0]
if k == "straight":
await itx.response.edit_message(content="Straight — pick a number:", view=StraightPicker(self.parent))
elif k == "split":
await itx.response.edit_message(content="Split — pick the first number:", view=SplitAnchorPicker(self.parent))
elif k == "street":
await itx.response.edit_message(content="Street — pick a row:", view=StreetPicker(self.parent))
else:
await itx.response.edit_message(content="Six-line — pick a range:", view=SixlinePicker(self.parent))
sel.callback = on_pick
@discord.ui.button(label="Back", style=discord.ButtonStyle.secondary, row=1)
async def back(self, itx: discord.Interaction, _):
await itx.response.edit_message(content="Bet Builder — choose an option:", view=BetPanelRoot(self.parent))
@discord.ui.button(label="Close", style=discord.ButtonStyle.secondary, emoji="✖️", row=1)
async def close(self, itx: discord.Interaction, _):
await itx.response.edit_message(content="Bet Builder closed.", view=None)
# ---- Straight picker (buttons) ----
class StraightPicker(discord.ui.View):
def __init__(self, parent: MiniRouletteView):
super().__init__(timeout=60); self.parent = parent
# Row 0: 0 + nav
self.add_item(self._button_num(0, row=0))
self.add_item(self._back_btn(row=0))
self.add_item(self._close_btn(row=0))
# Rows 1.. for 1..18 (≤5 per row)
n = 1; row = 1
while n <= 18:
for _ in range(5):
if n > 18: break
self.add_item(self._button_num(n, row=row)); n += 1
row += 1
def _button_num(self, n: int, row: int):
style = (discord.ButtonStyle.secondary if n == 0
else discord.ButtonStyle.success if n in ROULETTE_RED
else discord.ButtonStyle.primary)
btn = discord.ui.Button(label=str(n), style=style, row=row)
async def cb(itx: discord.Interaction):
bet = {"kind":"straight","targets":(n,), "amount":self.parent.chip,
"label": f"Straight {n} ${self.parent.chip}", "payout": payout_for("straight")}
self.parent.bets.append(bet)
await itx.response.edit_message(content=f"✅ Added: Straight {n}\n\nInside — choose type:",
view=InsideKindPicker(self.parent))
try:
await self.parent.message.edit(embed=self.parent.embed_slip(), view=self.parent)
except Exception:
pass
btn.callback = cb; return btn
def _back_btn(self, row:int):
btn = discord.ui.Button(label="Back", style=discord.ButtonStyle.secondary, row=row)
btn.callback = lambda itx: itx.response.edit_message(content="Inside bet — choose type:", view=InsideKindPicker(self.parent))
return btn
def _close_btn(self, row:int):
btn = discord.ui.Button(label="Close", style=discord.ButtonStyle.secondary, emoji="✖️", row=row)
btn.callback = lambda itx: itx.response.edit_message(content="Bet Builder closed.", view=None)
return btn
# ---- Split pickers (anchor → neighbor) ----
class SplitAnchorPicker(discord.ui.View):
def __init__(self, parent: MiniRouletteView):
super().__init__(timeout=60); self.parent = parent
self.add_item(self._back_btn(row=0)); self.add_item(self._close_btn(row=0))
self.add_item(self._button_num(0, row=0))
n = 1; row = 1
while n <= 18:
for _ in range(5):
if n > 18: break
self.add_item(self._button_num(n, row=row)); n += 1
row += 1
def _neighbors(self, a: int) -> List[int]:
if a == 0: return [1,2,3]
opts = set()
if a % 3 != 1: opts.add(a-1)
if a % 3 != 0: opts.add(a+1)
if a - 3 >= 1: opts.add(a-3)
if a + 3 <= 18: opts.add(a+3)
return sorted(opts)
def _button_num(self, n:int, row:int):
style = (discord.ButtonStyle.secondary if n == 0
else discord.ButtonStyle.success if n in ROULETTE_RED
else discord.ButtonStyle.primary)
btn = discord.ui.Button(label=str(n), style=style, row=row)
async def cb(itx: discord.Interaction):
neigh = self._neighbors(n)
await itx.response.edit_message(content=f"Split — anchor **{n}**. Pick the second number:",
view=SplitNeighborPicker(self.parent, n, neigh))
btn.callback = cb; return btn
def _back_btn(self, row:int):
btn = discord.ui.Button(label="Back", style=discord.ButtonStyle.secondary, row=row)
btn.callback = lambda itx: itx.response.edit_message(content="Inside bet — choose type:", view=InsideKindPicker(self.parent))
return btn
def _close_btn(self, row:int):
btn = discord.ui.Button(label="Close", style=discord.ButtonStyle.secondary, emoji="✖️", row=row)
btn.callback = lambda itx: itx.response.edit_message(content="Bet Builder closed.", view=None)
return btn
class SplitNeighborPicker(discord.ui.View):
def __init__(self, parent: MiniRouletteView, anchor: int, neigh: List[int]):
super().__init__(timeout=60); self.parent = parent; self.anchor = anchor
# Row 0: Back/Close
self.add_item(self._back_btn(row=0)); self.add_item(self._close_btn(row=0))
# Neighbor buttons from row 1 onward (≤5 per row)
row = 1
for i, m in enumerate(neigh):
if i and i % 5 == 0: row += 1
style = (discord.ButtonStyle.secondary if m == 0
else discord.ButtonStyle.success if m in ROULETTE_RED
else discord.ButtonStyle.primary)
btn = discord.ui.Button(label=str(m), style=style, row=row)
btn.callback = self._make_cb(m)
self.add_item(btn)
def _make_cb(self, m:int):
async def cb(itx: discord.Interaction):
a,b = sorted((self.anchor, m))
bet = {"kind":"split","targets":(a,b),"amount":self.parent.chip,
"label": f"Split {a}-{b} ${self.parent.chip}","payout": payout_for("split")}
self.parent.bets.append(bet)
await itx.response.edit_message(content=f"✅ Added: Split {a}-{b}\n\nInside — choose type:",
view=InsideKindPicker(self.parent))
try:
await self.parent.message.edit(embed=self.parent.embed_slip(), view=self.parent)
except Exception:
pass
return cb
def _back_btn(self, row:int):
btn = discord.ui.Button(label="Back", style=discord.ButtonStyle.secondary, row=row)
btn.callback = lambda itx: itx.response.edit_message(content="Split — pick the first number:", view=SplitAnchorPicker(self.parent))
return btn
def _close_btn(self, row:int):
btn = discord.ui.Button(label="Close", style=discord.ButtonStyle.secondary, emoji="✖️", row=row)
btn.callback = lambda itx: itx.response.edit_message(content="Bet Builder closed.", view=None)
return btn
# ---- Street ----
class StreetPicker(discord.ui.View):
def __init__(self, parent: MiniRouletteView):
super().__init__(timeout=60); self.parent = parent
self.add_item(self._back_btn(row=0)); self.add_item(self._close_btn(row=0))
rows = street_rows()
row = 1
for i, trio in enumerate(rows):
if i and i % 5 == 0: row += 1
label = "0-1-2" if trio == (0,1,2) else "0-2-3" if trio == (0,2,3) else f"{trio[0]}{trio[2]}"
btn = discord.ui.Button(label=label, style=discord.ButtonStyle.secondary, row=row)
btn.callback = self._make_cb(trio, label)
self.add_item(btn)
def _make_cb(self, trio: Tuple[int,int,int], label: str):
async def cb(itx: discord.Interaction):
bet = {"kind":"street","targets":tuple(trio),"amount":self.parent.chip,
"label": f"Street {label} ${self.parent.chip}", "payout": payout_for("street")}
self.parent.bets.append(bet)
await itx.response.edit_message(content=f"✅ Added: Street {label}\n\nInside — choose type:",
view=InsideKindPicker(self.parent))
try:
await self.parent.message.edit(embed=self.parent.embed_slip(), view=self.parent)
except Exception:
pass
return cb
def _back_btn(self, row:int):
btn = discord.ui.Button(label="Back", style=discord.ButtonStyle.secondary, row=row)
btn.callback = lambda itx: itx.response.edit_message(content="Inside bet — choose type:", view=InsideKindPicker(self.parent))
return btn
def _close_btn(self, row:int):
btn = discord.ui.Button(label="Close", style=discord.ButtonStyle.secondary, emoji="✖️", row=row)
btn.callback = lambda itx: itx.response.edit_message(content="Bet Builder closed.", view=None)
return btn
# ---- Six-line ----
class SixlinePicker(discord.ui.View):
def __init__(self, parent: MiniRouletteView):
super().__init__(timeout=60); self.parent = parent
self.add_item(self._back_btn(row=0)); self.add_item(self._close_btn(row=0))
sixes = sixlines_all()
row = 1
for i, s in enumerate(sixes):
if i and i % 5 == 0: row += 1
label = f"{s[0]}{s[-1]}"
btn = discord.ui.Button(label=label, style=discord.ButtonStyle.secondary, row=row)
btn.callback = self._make_cb(s, label)
self.add_item(btn)
def _make_cb(self, group: Tuple[int,...], label: str):
async def cb(itx: discord.Interaction):
bet = {"kind":"sixline","targets":tuple(group),"amount":self.parent.chip,
"label": f"Six-line {label} ${self.parent.chip}", "payout": payout_for("sixline")}
self.parent.bets.append(bet)
await itx.response.edit_message(content=f"✅ Added: Six-line {label}\n\nInside — choose type:",
view=InsideKindPicker(self.parent))
try:
await self.parent.message.edit(embed=self.parent.embed_slip(), view=self.parent)
except Exception:
pass
return cb
def _back_btn(self, row:int):
btn = discord.ui.Button(label="Back", style=discord.ButtonStyle.secondary, row=row)
btn.callback = lambda itx: itx.response.edit_message(content="Inside bet — choose type:", view=InsideKindPicker(self.parent))
return btn
def _close_btn(self, row:int):
btn = discord.ui.Button(label="Close", style=discord.ButtonStyle.secondary, emoji="✖️", row=row)
btn.callback = lambda itx: itx.response.edit_message(content="Bet Builder closed.", view=None)
return btn
# ===== Cog =====
_active_sessions: Dict[int, MiniRouletteView] = {}
class MiniRoulette(commands.Cog):
def __init__(self, bot): self.bot = bot
@commands.command(name="roulette")
async def roulette(self, ctx: commands.Context):
uid = ctx.author.id
if uid in _active_sessions:
await ctx.send("❌ You already have an active roulette session. Finish it or wait for it to time out.")
return
view = MiniRouletteView(self.bot, uid, timeout=120)
msg = await ctx.send(embed=view.embed_slip(), view=view)
view.message = msg
_active_sessions[uid] = view
@commands.command(name="rules_roulette", aliases=["roulette_rules","rrules"])
async def rules_roulette(self, ctx: commands.Context):
e = discord.Embed(title="🎯 Mini Roulette (018) — Rules", color=discord.Color.gold())
e.description = (
"**Wheel:** 19 pockets (018). Casino edge ≈ 1/19 by paying slightly under fair odds.\n\n"
"**Outside Bets:** Red/Black 1:1 • Even/Odd 1:1 (0 loses) • Low/High 1:1 • "
"Column (6 nums) 2:1 • Sixes (16 / 712 / 1318) 2:1\n"
"**Inside Bets:** Straight 17:1 • Split 8:1 • Street 5:1 • Six-line 2:1\n\n"
"Open **Add Outside / Inside** to build your slip. Set **Chip** with the buttons, then **Spin**. "
"Use **Spin Again** to reuse your slip."
)
await ctx.send(embed=e)
async def setup(bot):
await bot.add_cog(MiniRoulette(bot))

340
src/cogs/slots.py Normal file
View File

@@ -0,0 +1,340 @@
import random, discord
from typing import Optional, List
from discord.ext import commands
from .. import db
from ..utils.constants import (
SYMBOLS, WEIGHTS, PAYOUTS,
SLOTS_DEFAULT_BET, SLOTS_MIN_BET, SLOTS_MAX_BET,
SPECIAL_SYMBOL, SPECIAL_BONUS_SPINS, WILDCARD_FACTOR
)
# All 8 possible lines (indices must stay stable)
LINES = [
# rows
[(0,0),(0,1),(0,2)], # 0: top row
[(1,0),(1,1),(1,2)], # 1: middle row
[(2,0),(2,1),(2,2)], # 2: bottom row
# columns
[(0,0),(1,0),(2,0)], # 3: left col
[(0,1),(1,1),(2,1)], # 4: mid col
[(0,2),(1,2),(2,2)], # 5: right col
# diagonals
[(0,0),(1,1),(2,2)], # 6: main diag
[(0,2),(1,1),(2,0)], # 7: anti diag
]
# Presets users can select
LINE_PRESETS = {
"1": [1], # center row
"3": [0,1,2], # all rows
"5": [0,1,2,6,7], # rows + diagonals
"8": list(range(8)), # all lines
}
# ---------- engine ----------
def spin_board():
pick = lambda: random.choices(SYMBOLS, weights=WEIGHTS, k=1)[0]
return [[pick() for _ in range(3)] for _ in range(3)]
def evaluate(board, active_indices: List[int]):
"""
Return (total_multiplier, bonus_spins, winning_lines)
- Only checks the active line indices.
- Wildcard rule: exactly one ⭐ + two identical fruits pays WILDCARD_FACTOR * fruit multiplier.
- ⭐⭐⭐ bonus on the middle row (index 1) triggers only if that line is active.
"""
total_mult = 0
bonus_spins = 0
winning_lines = []
for idx in active_indices:
line = LINES[idx]
a, b, c = (board[r][c] for (r,c) in line)
# Straight three-of-a-kind
if a == b == c:
mult = PAYOUTS.get(a, 0)
total_mult += mult
winning_lines.append((idx, a, mult))
continue
# Wildcard: exactly one star + two identical non-star fruits
syms = [a, b, c]
star_count = syms.count(SPECIAL_SYMBOL)
if star_count == 1:
non_stars = [s for s in syms if s != SPECIAL_SYMBOL]
if len(non_stars) == 2 and non_stars[0] == non_stars[1]:
fruit = non_stars[0]
base = PAYOUTS.get(fruit, 0)
mult = max(1, int(base * WILDCARD_FACTOR)) # reduced payout for wild wins
total_mult += mult
winning_lines.append((idx, fruit, mult))
continue
# ⭐⭐+fruit or mixed → no payout
# ⭐⭐⭐ bonus spins on the middle row, but only if that line is active
if 1 in active_indices:
if all(board[r][c] == SPECIAL_SYMBOL for (r,c) in LINES[1]):
bonus_spins += SPECIAL_BONUS_SPINS
return total_mult, bonus_spins, winning_lines
def fancy_board(board, winning_lines):
"""Monospace board with line markers."""
win_idx = {idx for idx, _, _ in winning_lines}
top = "┏━━━━┳━━━━┳━━━━┓"
mid = "┣━━━━╋━━━━╋━━━━┫"
bot = "┗━━━━┻━━━━┻━━━━┛"
def row_str(r):
cells = [f"{board[r][c]}" for c in range(3)]
line = f" {cells[0]} {cells[1]} {cells[2]} "
if r == 0 and 0 in win_idx: line += " ← win"
if r == 1 and 1 in win_idx: line += " ← win"
if r == 2 and 2 in win_idx: line += " ← win"
return line
rows = [top, row_str(0), mid, row_str(1), mid, row_str(2), bot]
col_marks = []
if 3 in win_idx: col_marks.append("col 1")
if 4 in win_idx: col_marks.append("col 2")
if 5 in win_idx: col_marks.append("col 3")
diag_marks = []
if 6 in win_idx: diag_marks.append("main diag")
if 7 in win_idx: diag_marks.append("anti diag")
footer = []
if col_marks: footer.append("Cols: " + ", ".join(col_marks))
if diag_marks: footer.append("Diags: " + ", ".join(diag_marks))
text = "\n".join(rows)
if footer:
text += "\n" + " " * 2 + "".join(footer)
return f"```\n{text}\n```"
def build_slots_embed(uid: int, bet: int, used_free: bool, board, total_mult, bonus_spins, winning_lines, active_indices: List[int]):
# Split bet across active lines
n_lines = max(1, len(active_indices))
line_bet = bet / n_lines
winnings = int(total_mult * line_bet)
if winnings:
db.add_cash(uid, winnings)
if bonus_spins:
db.add_free_spins(uid, bonus_spins)
db.record_slots(uid, winnings)
cash, free_spins = db.get_wallet(uid)
grid = fancy_board(board, winning_lines)
outcome = f"🎉 You won **${winnings}**!" if winnings > 0 else "😬 No winning lines."
if bonus_spins:
outcome += f"\n⭐ **Bonus!** You earned **{bonus_spins}** free spins!"
cost_line = "🆓 Used a free spin." if used_free else f"💸 Spin cost **${bet}**."
desc = (
f"{grid}\n\n"
f"{cost_line}\n{outcome}\n\n"
f"**Balance:** ${cash} | **Free Spins:** {free_spins}"
)
embed = discord.Embed(title="🎰 Fruit Slots", description=desc, color=discord.Color.purple())
embed.set_footer(text=f"Lines: {n_lines} • Per-line: ${line_bet:.0f} • Current bet: ${bet} • 🔁 spin • Set Bet / Lines to change")
return embed
# ---------- UI pieces ----------
class LinesSelect(discord.ui.Select):
def __init__(self, parent_view):
self.parent_view = parent_view
options = [
discord.SelectOption(label="1 line (center row)", value="1", default=(len(parent_view.active_indices)==1)),
discord.SelectOption(label="3 lines (rows)", value="3", default=(len(parent_view.active_indices)==3)),
discord.SelectOption(label="5 lines (rows+diags)",value="5", default=(len(parent_view.active_indices)==5)),
discord.SelectOption(label="8 lines (all)", value="8", default=(len(parent_view.active_indices)==8)),
]
super().__init__(placeholder="Choose lines…", min_values=1, max_values=1, options=options, row=1)
async def callback(self, interaction: discord.Interaction):
if interaction.user.id != self.parent_view.user_id:
await interaction.response.send_message("This isn't your slots session!", ephemeral=True); return
preset = self.values[0]
self.parent_view.active_indices = LINE_PRESETS.get(preset, list(range(8)))
for o in self.options:
o.default = (o.value == preset)
# Update footer only
if interaction.message and interaction.message.embeds:
emb = interaction.message.embeds[0]
n_lines = max(1, len(self.parent_view.active_indices))
per_line = self.parent_view.bet / n_lines
emb.set_footer(text=f"Lines: {n_lines} • Per-line: ${per_line:.0f} • Current bet: ${self.parent_view.bet} • 🔁 spin • Set Bet / Lines to change")
await interaction.response.edit_message(embed=emb, view=self.parent_view)
else:
await interaction.response.edit_message(view=self.parent_view)
class SetBetModal(discord.ui.Modal, title="Set Slots Bet"):
def __init__(self, view: "SlotsView"):
super().__init__()
self.view = view
self.amount = discord.ui.TextInput(
label=f"Amount (min {SLOTS_MIN_BET}, max {SLOTS_MAX_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 isnt your slots session!", 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)
# clamp
amt = max(SLOTS_MIN_BET, min(SLOTS_MAX_BET, amt))
self.view.bet = amt
# acknowledge modal
await itx.response.defer(ephemeral=True)
# refresh the message footer AND the cost line in the description
if self.view.message and self.view.message.embeds:
emb = self.view.message.embeds[0]
# --- update the “spin cost” line in the description ---
desc = emb.description or ""
lines = desc.splitlines()
for i, line in enumerate(lines):
# replace either the previous cost line or a “used free spin” line
if line.startswith("💸") or line.startswith("🆓"):
lines[i] = f"💸 Spin cost **${self.view.bet}**."
break
emb.description = "\n".join(lines)
# --- update footer (per-line) ---
n_lines = max(1, len(self.view.active_indices))
per_line = self.view.bet / n_lines
emb.set_footer(
text=f"Lines: {n_lines} • Per-line: ${per_line:.0f} • Current bet: ${self.view.bet} • 🔁 spin • Set Bet / Lines to change"
)
await self.view.message.edit(embed=emb, view=self.view)
# ---------- View ----------
class SlotsView(discord.ui.View):
def __init__(self, bot: commands.Bot, user_id: int, bet: int, active_indices: List[int], *, timeout: float = 120.0):
super().__init__(timeout=timeout)
self.bot = bot
self.user_id = user_id
self.bet = bet
self.active_indices = active_indices[:] # copy
self.message: Optional[discord.Message] = None
self._busy = False
# Controls: Set Bet + Spin on row 0; Lines selector on row 1
self.add_item(LinesSelect(self))
def disable_all_items(self):
for item in self.children:
item.disabled = True
async def on_timeout(self):
self.disable_all_items()
try:
if self.message:
await self.message.edit(view=self)
except Exception:
pass
# Set Bet (left of Spin)
@discord.ui.button(label="Set Bet", style=discord.ButtonStyle.secondary, row=0)
async def set_bet_btn(self, interaction: discord.Interaction, _):
if interaction.user.id != self.user_id:
return await interaction.response.send_message("This isn't your slots session!", ephemeral=True)
await interaction.response.send_modal(SetBetModal(self))
@discord.ui.button(label="Spin Again", style=discord.ButtonStyle.primary, emoji="🔁", row=0)
async def spin_again(self, interaction: discord.Interaction, _):
if interaction.user.id != self.user_id:
await interaction.response.send_message("This isn't your slots session!", ephemeral=True); return
if self._busy:
await interaction.response.send_message("Hold on, finishing the current spin…", ephemeral=True); return
self._busy = True
try:
# Economy: consume a free spin, else charge full bet
used_free = db.consume_free_spin(self.user_id)
if not used_free:
cash, _ = db.get_wallet(self.user_id)
if cash < self.bet:
await interaction.response.send_message(
f"❌ Not enough cash for **${self.bet}**. Use `!topup` or lower your bet/lines.",
ephemeral=True
)
return
db.add_cash(self.user_id, -self.bet)
board = spin_board()
total_mult, bonus_spins, winning_lines = evaluate(board, self.active_indices)
embed = build_slots_embed(self.user_id, self.bet, used_free, board, total_mult, bonus_spins, winning_lines, self.active_indices)
await interaction.response.edit_message(embed=embed, view=self)
finally:
self._busy = False
# ---------- Cog ----------
class Slots(commands.Cog):
def __init__(self, bot): self.bot = bot
@commands.command(name="slots")
async def slots(self, ctx, bet: int = None):
"""Spin the slots! Usage: !slots [bet]
- Set Bet (modal) and choose Lines on the message.
- Spin cost is the bet; it's split evenly across active lines.
- ⭐ is a wildcard for 'two of a kind + one ⭐' (reduced payout).
- ⭐⭐⭐ on the middle row awards bonus spins, but only if that line is active.
"""
uid = ctx.author.id
# sanitize bet
if bet is None:
bet = SLOTS_DEFAULT_BET
if bet < SLOTS_MIN_BET:
await ctx.send(f"❌ Minimum bet is ${SLOTS_MIN_BET}."); return
if bet > SLOTS_MAX_BET:
await ctx.send(f"❌ Maximum bet is ${SLOTS_MAX_BET}."); return
# default lines preset = rows + diags (5 lines)
active_indices = [0,1,2,6,7]
# Economy: consume a free spin if available; otherwise charge full bet
used_free = db.consume_free_spin(uid)
if not used_free:
cash, _ = db.get_wallet(uid)
if cash < bet:
await ctx.send(f"❌ You need ${bet} (or a free spin) to play at that bet.")
return
db.add_cash(uid, -bet)
# First spin
board = spin_board()
total_mult, bonus_spins, winning_lines = evaluate(board, active_indices)
embed = build_slots_embed(uid, bet, used_free, board, total_mult, bonus_spins, winning_lines, active_indices)
# Send with view (Set Bet + Lines + Spin Again)
view = SlotsView(self.bot, uid, bet, active_indices, timeout=120)
msg = await ctx.send(embed=embed, view=view)
view.message = msg
async def setup(bot):
await bot.add_cog(Slots(bot))

372
src/cogs/towers.py Normal file
View File

@@ -0,0 +1,372 @@
# src/cogs/towers.py
import math, random, discord
from discord.ext import commands
from typing import Optional, Dict, List, Tuple
from .. import db
from ..utils.constants import TOWERS_MIN_BET, TOWERS_MAX_BET, TOWERS_EDGE_PER_STEP
# difficulty map: (row_size, bad_per_row)
DIFFS: Dict[str, Tuple[int, int]] = {
"Easy": (4, 1), # 3/4 safe
"Medium": (3, 1), # 2/3 safe
"Hard": (2, 1), # 1/2 safe
"Expert": (3, 2), # 1/3 safe
"Master": (4, 3), # 1/4 safe
}
DIFF_ORDER = list(DIFFS.keys())
TIERS = 9 # rows high
_active_tower_sessions: Dict[int, "TowersView"] = {}
def step_multiplier(row_size: int, bad: int) -> float:
"""Per-step multiplier from odds with a small house edge."""
p = (row_size - bad) / row_size
fair = 1.0 / p
return round(fair * (1.0 - TOWERS_EDGE_PER_STEP), 2)
def cumulative_multiplier(steps_cleared: int, row_size: int, bad: int) -> float:
m = step_multiplier(row_size, bad)
return round(m ** steps_cleared, 2) if steps_cleared > 0 else 1.0
def fmt_money(n: int) -> str:
return f"${n:,}"
class SetBetModal(discord.ui.Modal, title="Set Towers Bet"):
def __init__(self, view: "TowersView"):
super().__init__()
self.view = view
self.amount = discord.ui.TextInput(
label=f"Amount (min {TOWERS_MIN_BET}, max {TOWERS_MAX_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):
# only owner can submit changes
if itx.user.id != self.view.user_id:
return await itx.response.send_message("This isnt your Towers session!", ephemeral=True)
if not self.view._ensure_can_modify_setup():
return await itx.response.send_message("You cant change bet after starting. Cashout or Reset.", 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)
amt = max(TOWERS_MIN_BET, min(TOWERS_MAX_BET, amt))
self.view.bet = amt
# Acknowledge, then update the original message
await itx.response.defer(ephemeral=True)
if self.view.message:
await self.view.message.edit(embed=self.view.embed(), view=self.view)
class TowersView(discord.ui.View):
def __init__(self, user_id: int, *, timeout: int = 180):
super().__init__(timeout=timeout)
self.user_id = user_id
# Configurable
self.bet = max(100, TOWERS_MIN_BET)
self.difficulty = "Easy"
# Round state
self.started = False # bet debited yet?
self.ended = False # lost or cashed out
self.row_index = 0 # 0..TIERS-1 (index of NEXT row to play)
self.picked_cols: List[int] = [] # chosen col per cleared row
self.failed_at: Optional[Tuple[int, int]] = None # (row, col) of loss
# Layout: bad positions per row
self.layout: List[set[int]] = []
self._regen_layout()
self.message: Optional[discord.Message] = None
self._busy = False
# ---------- lifecycle ----------
def _regen_layout(self):
size, bad = DIFFS[self.difficulty]
self.layout = [set(random.sample(range(size), bad)) for _ in range(TIERS)]
def reset_round(self, *, keep_bet=True, keep_diff=True):
if not keep_bet:
self.bet = max(self.bet, TOWERS_MIN_BET)
if not keep_diff:
self.difficulty = "Easy"
self.started = False
self.ended = False
self.row_index = 0
self.picked_cols.clear()
self.failed_at = None
self._regen_layout()
async def on_timeout(self):
self.disable_all_items()
try:
if self.message:
await self.message.edit(view=self)
finally:
_active_tower_sessions.pop(self.user_id, None)
def disable_all_items(self):
for item in self.children:
item.disabled = True
# ---------- derived values ----------
@property
def row_size(self) -> int:
return DIFFS[self.difficulty][0]
@property
def bad_per_row(self) -> int:
return DIFFS[self.difficulty][1]
@property
def can_cashout(self) -> bool:
return (not self.ended) and self.row_index > 0 and self.started
def current_multiplier(self) -> float:
return cumulative_multiplier(self.row_index, self.row_size, self.bad_per_row)
def potential_cashout(self) -> int:
return int(math.floor(self.bet * self.current_multiplier()))
# ---------- rendering ----------
def _row_str(self, row: int) -> str:
size = self.row_size
tiles = []
fail_c = self.failed_at[1] if (self.failed_at and self.failed_at[0] == row) else None
picked_c = self.picked_cols[row] if row < len(self.picked_cols) else None
is_current = (row == self.row_index) and (not self.ended)
for c in range(size):
if picked_c is not None:
tiles.append("🟩" if c == picked_c else "")
elif fail_c is not None:
tiles.append("💀" if c == fail_c else "")
else:
tiles.append("🟨" if is_current else "")
# center shorter rows so the board looks neat (target width ~4)
pad = 4 - size
if pad > 0:
left = pad // 2
right = pad - left
return " " * (left * 2) + " ".join(tiles) + " " * (right * 2)
return " ".join(tiles)
def draw_board(self) -> str:
# top row printed first
lines = [self._row_str(r) for r in range(TIERS - 1, -1, -1)]
return "```\n" + "\n".join(lines) + "\n```"
def embed(self) -> discord.Embed:
cash, _ = db.get_wallet(self.user_id)
e = discord.Embed(title="🐉 Towers", color=discord.Color.dark_teal())
e.description = self.draw_board()
mm = self.current_multiplier()
pot = self.potential_cashout()
status = [
f"Bet: **{fmt_money(self.bet)}**",
f"Difficulty: **{self.difficulty}**",
f"Progress: **{self.row_index}/{TIERS}**",
f"Multiplier: **x{mm:.2f}**",
f"Cashout now: **{fmt_money(pot)}**" if self.can_cashout else "Cashout now: **—**",
]
e.add_field(name="Status", value="".join(status), inline=False)
e.add_field(name="Balance", value=fmt_money(cash), inline=True)
e.set_footer(text="Pick a tile on the current row. Cashout anytime. Reset to start a new board.")
return e
# ---------- guards ----------
async def _ensure_owner(self, itx: discord.Interaction):
if itx.user.id != self.user_id:
raise await itx.response.send_message("This isnt your Towers session!", ephemeral=True)
def _ensure_can_modify_setup(self) -> bool:
return not self.started and not self.ended
async def _update(self, itx: discord.Interaction):
await itx.response.edit_message(embed=self.embed(), view=self)
async def _debit_if_needed(self) -> bool:
if self.started:
return True
cash, _ = db.get_wallet(self.user_id)
if cash < self.bet:
return False
db.add_cash(self.user_id, -self.bet)
self.started = True
return True
async def _pick_col(self, itx: discord.Interaction, col: int):
await self._ensure_owner(itx)
if self._busy or self.ended:
return
if not (0 <= col < self.row_size):
return
if not await self._debit_if_needed():
return await itx.response.send_message("💸 Not enough cash for that bet.", ephemeral=True)
self._busy = True
try:
bads = self.layout[self.row_index]
if col in bads:
self.failed_at = (self.row_index, col)
self.ended = True
db.record_towers(self.user_id, False)
return await self._update(itx)
self.picked_cols.append(col)
self.row_index += 1
if self.row_index >= TIERS:
winnings = self.potential_cashout()
if winnings > 0:
db.add_cash(self.user_id, winnings)
self.ended = True
db.record_towers(self.user_id, True)
return await self._update(itx)
await self._update(itx)
finally:
self._busy = False
# ---------- Controls ----------
# Row 0: bet controls (Set Bet, ×2, ½)
@discord.ui.button(label="Set Bet", style=discord.ButtonStyle.secondary, row=0)
async def set_bet(self, itx: discord.Interaction, _):
await self._ensure_owner(itx)
if not self._ensure_can_modify_setup():
return await itx.response.send_message("You cant change bet after starting. Cashout or Reset.", ephemeral=True)
await itx.response.send_modal(SetBetModal(self))
@discord.ui.button(label="×2", style=discord.ButtonStyle.secondary, row=0)
async def bet_x2(self, itx: discord.Interaction, _):
await self._ensure_owner(itx)
if not self._ensure_can_modify_setup():
return await itx.response.send_message("You cant change bet after starting. Cashout or Reset.", ephemeral=True)
self.bet = max(TOWERS_MIN_BET, min(TOWERS_MAX_BET, self.bet * 2))
await self._update(itx)
@discord.ui.button(label="½", style=discord.ButtonStyle.secondary, row=0)
async def bet_half(self, itx: discord.Interaction, _):
await self._ensure_owner(itx)
if not self._ensure_can_modify_setup():
return await itx.response.send_message("You cant change bet after starting. Cashout or Reset.", ephemeral=True)
self.bet = max(TOWERS_MIN_BET, self.bet // 2)
await self._update(itx)
# Row 1: difficulty select (1 component)
@discord.ui.select(
placeholder="Difficulty",
options=[
discord.SelectOption(label="Easy", description="4 slots / 1 bad"),
discord.SelectOption(label="Medium", description="3 slots / 1 bad"),
discord.SelectOption(label="Hard", description="2 slots / 1 bad"),
discord.SelectOption(label="Expert", description="3 slots / 2 bad"),
discord.SelectOption(label="Master", description="4 slots / 3 bad"),
],
row=1, min_values=1, max_values=1
)
async def diff_select(self, itx: discord.Interaction, select: discord.ui.Select):
await self._ensure_owner(itx)
if not self._ensure_can_modify_setup():
return await itx.response.send_message("You cant change difficulty after starting. Cashout or Reset.", ephemeral=True)
self.difficulty = select.values[0]
self.reset_round()
await self._update(itx)
# Row 2: Random / Cashout / Reset
@discord.ui.button(label="Pick Random Tile", style=discord.ButtonStyle.primary, emoji="🎲", row=2)
async def pick_random(self, itx: discord.Interaction, _):
await self._ensure_owner(itx)
if self.ended: return
await self._pick_col(itx, random.randrange(self.row_size))
@discord.ui.button(label="Cashout", style=discord.ButtonStyle.success, emoji="💰", row=2)
async def cashout(self, itx: discord.Interaction, _):
await self._ensure_owner(itx)
if not self.can_cashout:
return await itx.response.send_message("Nothing to cash out yet.", ephemeral=True)
self.ended = True
winnings = self.potential_cashout()
if winnings > 0:
db.add_cash(self.user_id, winnings)
db.record_game(self.user_id, True)
await self._update(itx)
@discord.ui.button(label="Reset", style=discord.ButtonStyle.secondary, emoji="🔄", row=2)
async def reset_btn(self, itx: discord.Interaction, _):
await self._ensure_owner(itx)
self.reset_round()
await self._update(itx)
# Row 3: column pick buttons (up to 4)
@discord.ui.button(label="1", style=discord.ButtonStyle.secondary, row=3)
async def col1(self, itx: discord.Interaction, _): await self._pick_col(itx, 0)
@discord.ui.button(label="2", style=discord.ButtonStyle.secondary, row=3)
async def col2(self, itx: discord.Interaction, _): await self._pick_col(itx, 1)
@discord.ui.button(label="3", style=discord.ButtonStyle.secondary, row=3)
async def col3(self, itx: discord.Interaction, _): await self._pick_col(itx, 2)
@discord.ui.button(label="4", style=discord.ButtonStyle.secondary, row=3)
async def col4(self, itx: discord.Interaction, _): await self._pick_col(itx, 3)
async def interaction_check(self, interaction: discord.Interaction) -> bool:
# keep column buttons and cashout state in sync each interaction
size = self.row_size
col_buttons: List[discord.ui.Button] = [c for c in self.children if isinstance(c, discord.ui.Button) and c.row == 3]
for i, btn in enumerate(col_buttons):
btn.disabled = self.ended or (i >= size) or (self.row_index >= TIERS and not self.failed_at)
for btn in self.children:
if isinstance(btn, discord.ui.Button) and btn.label == "Cashout":
btn.disabled = not self.can_cashout
return True
class Towers(commands.Cog):
def __init__(self, bot): self.bot = bot
@commands.command(name="towers")
async def towers(self, ctx: commands.Context):
uid = ctx.author.id
if uid in _active_tower_sessions:
return await ctx.send("❌ You already have an active Towers session. Wait for it to time out or Reset.")
try:
view = TowersView(uid, timeout=180)
msg = await ctx.send(embed=view.embed(), view=view)
except Exception as e:
return await ctx.send(f"⚠️ Towers failed to start: `{type(e).__name__}: {e}`")
view.message = msg
_active_tower_sessions[uid] = view
@commands.command(name="rules_towers", aliases=["towers_rules"])
async def rules_towers(self, ctx: commands.Context):
e = discord.Embed(title="🐉 Towers — Rules", color=discord.Color.dark_teal())
e.description = (
"Nine tiers high. Pick one tile on each row:\n"
"• **Easy:** 4 slots / 1 bad (3 safe)\n"
"• **Medium:** 3 slots / 1 bad (2 safe)\n"
"• **Hard:** 2 slots / 1 bad (1 safe)\n"
"• **Expert:** 3 slots / 2 bad (1 safe)\n"
"• **Master:** 4 slots / 3 bad (1 safe)\n\n"
"You pay your bet once on the first pick. Each safe pick **multiplies** your cashout. "
"Cash out anytime; hit a bad tile and you lose the bet."
)
lines = []
for name in DIFF_ORDER:
size, bad = DIFFS[name]
lines.append(f"**{name}** per row ≈ x{step_multiplier(size, bad):.2f}")
e.add_field(name="Multipliers", value="\n".join(lines), inline=False)
await ctx.send(embed=e)
async def setup(bot):
await bot.add_cog(Towers(bot))

340
src/db.py Normal file
View File

@@ -0,0 +1,340 @@
import os, sqlite3
from datetime import date, datetime, timedelta
DB_PATH = os.getenv("DB_PATH", "/app/data/blackjack.db")
def _connect():
return sqlite3.connect(DB_PATH)
def init_db():
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
conn = _connect()
cur = conn.cursor()
# base
cur.execute("""
CREATE TABLE IF NOT EXISTS users (
user_id INTEGER PRIMARY KEY,
cash INTEGER DEFAULT 1000,
games_played INTEGER DEFAULT 0,
games_won INTEGER DEFAULT 0,
last_daily DATE DEFAULT NULL
)
""")
# add new columns if missing
cur.execute("PRAGMA table_info(users)")
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:
cur.execute(f"ALTER TABLE users ADD COLUMN {name} {decl}")
conn.commit()
conn.close()
def ensure_user(user_id: int):
conn = _connect(); cur = conn.cursor()
cur.execute("SELECT 1 FROM users WHERE user_id=?", (user_id,))
if cur.fetchone() is None:
cur.execute("INSERT INTO users (user_id) VALUES (?)", (user_id,))
conn.commit()
conn.close()
def get_wallet(user_id: int):
ensure_user(user_id)
conn = _connect(); cur = conn.cursor()
cur.execute("SELECT cash, free_spins FROM users WHERE user_id=?", (user_id,))
cash, free_spins = cur.fetchone()
conn.close()
return cash, free_spins
def add_cash(user_id: int, delta: int):
ensure_user(user_id)
conn = _connect(); cur = conn.cursor()
cur.execute("UPDATE users SET cash = cash + ? WHERE user_id=?", (delta, user_id))
conn.commit(); conn.close()
def add_free_spins(user_id: int, spins: int):
ensure_user(user_id)
if spins <= 0: return
conn = _connect(); cur = conn.cursor()
cur.execute("UPDATE users SET free_spins = free_spins + ? WHERE user_id=?", (spins, user_id))
conn.commit(); conn.close()
def consume_free_spin(user_id: int) -> bool:
ensure_user(user_id)
conn = _connect(); cur = conn.cursor()
cur.execute("SELECT free_spins FROM users WHERE user_id=?", (user_id,))
fs = cur.fetchone()[0]
if fs > 0:
cur.execute("UPDATE users SET free_spins = free_spins - 1 WHERE user_id=?", (user_id,))
conn.commit(); conn.close()
return True
conn.close()
return False
# legacy aggregate
def record_game(user_id: int, won: bool):
ensure_user(user_id)
conn = _connect(); cur = conn.cursor()
if won:
cur.execute("UPDATE users SET games_played=games_played+1, games_won=games_won+1 WHERE user_id=?", (user_id,))
else:
cur.execute("UPDATE users SET games_played=games_played+1 WHERE user_id=?", (user_id,))
conn.commit(); conn.close()
# per-game
def record_blackjack(user_id: int, won: bool):
ensure_user(user_id)
conn = _connect(); cur = conn.cursor()
if won:
cur.execute("""
UPDATE users SET
games_played_blackjack = games_played_blackjack + 1,
games_won_blackjack = games_won_blackjack + 1
WHERE user_id = ?
""", (user_id,))
else:
cur.execute("""
UPDATE users SET
games_played_blackjack = games_played_blackjack + 1
WHERE user_id = ?
""", (user_id,))
conn.commit(); conn.close()
record_game(user_id, won)
def record_slots(user_id: int, win_amount: int):
ensure_user(user_id)
won = (win_amount > 0)
conn = _connect(); cur = conn.cursor()
if won:
cur.execute("""
UPDATE users SET
games_played_slots = games_played_slots + 1,
games_won_slots = games_won_slots + 1,
slots_biggest_win = CASE WHEN ? > slots_biggest_win THEN ? ELSE slots_biggest_win END
WHERE user_id = ?
""", (win_amount, win_amount, user_id))
else:
cur.execute("""
UPDATE users SET
games_played_slots = games_played_slots + 1
WHERE user_id = ?
""", (user_id,))
conn.commit(); conn.close()
record_game(user_id, won)
def record_roulette(user_id: int, total_bet: int, total_return: int):
ensure_user(user_id)
net = total_return - total_bet
conn = _connect(); cur = conn.cursor()
cur.execute("""
UPDATE users
SET games_played_roulette = games_played_roulette + 1,
roulette_net = roulette_net + ?,
roulette_biggest_win = CASE WHEN ? > roulette_biggest_win THEN ? ELSE roulette_biggest_win END
WHERE user_id = ?
""", (net, total_return, total_return, user_id))
conn.commit(); conn.close()
def record_coinflip(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_coinflip = games_played_coinflip + 1,
games_won_coinflip = games_won_coinflip + 1,
coinflip_net = coinflip_net + ?,
coinflip_biggest_win = CASE WHEN ? > coinflip_biggest_win THEN ? ELSE coinflip_biggest_win END
WHERE user_id = ?
""", (net, return_amount, return_amount, user_id))
else:
cur.execute("""
UPDATE users
SET games_played_coinflip = games_played_coinflip + 1,
coinflip_net = coinflip_net + ?
WHERE user_id = ?
""", (net, user_id))
conn.commit(); conn.close()
record_game(user_id, won)
def record_towers(user_id: int, won: bool):
"""Call with won=True on cashout/full clear; won=False on bomb."""
ensure_user(user_id)
conn = _connect(); cur = conn.cursor()
if won:
cur.execute("""
UPDATE users SET
games_played_towers = games_played_towers + 1,
games_won_towers = games_won_towers + 1
WHERE user_id = ?
""", (user_id,))
else:
cur.execute("""
UPDATE users SET
games_played_towers = games_played_towers + 1
WHERE user_id = ?
""", (user_id,))
conn.commit(); conn.close()
record_game(user_id, won)
# NEW: Baccarat
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)
net = total_return - total_bet
won = net > 0
conn = _connect(); cur = conn.cursor()
cur.execute("""
UPDATE users
SET games_played_baccarat = games_played_baccarat + 1,
games_won_baccarat = games_won_baccarat + CASE WHEN ? > 0 THEN 1 ELSE 0 END,
baccarat_net = baccarat_net + ?,
baccarat_biggest_win = CASE WHEN ? > baccarat_biggest_win THEN ? ELSE baccarat_biggest_win END
WHERE user_id = ?
""", (net, net, total_return, total_return, user_id))
conn.commit(); conn.close()
record_game(user_id, won)
def top_cash(limit=10):
conn = _connect(); cur = conn.cursor()
cur.execute("SELECT user_id, cash FROM users ORDER BY cash DESC LIMIT ?", (limit,))
rows = cur.fetchall()
conn.close()
return rows
def user_counts():
conn = _connect(); cur = conn.cursor()
cur.execute("SELECT COUNT(*) FROM users")
n = cur.fetchone()[0]
conn.close()
return n
def get_full_stats(user_id: int):
ensure_user(user_id)
conn = _connect(); cur = conn.cursor()
cur.execute("""
SELECT cash, free_spins, games_played, games_won,
games_played_blackjack, games_won_blackjack,
games_played_slots, games_won_slots,
games_played_roulette, roulette_net,
games_played_coinflip, games_won_coinflip, coinflip_net,
games_played_towers, games_won_towers,
games_played_baccarat, games_won_baccarat, baccarat_net
FROM users WHERE user_id=?
""", (user_id,))
row = cur.fetchone(); conn.close()
keys = [
"cash","free_spins","games_played","games_won",
"gp_bj","gw_bj",
"gp_slots","gw_slots",
"gp_roulette","roulette_net",
"gp_coin","gw_coin","coin_net",
"gp_towers","gw_towers",
"gp_bac","gw_bac","bac_net",
]
return dict(zip(keys, row))
def can_claim_topup(user_id: int, cooldown_minutes: int = 5) -> tuple[bool, int]:
ensure_user(user_id)
conn = _connect(); cur = conn.cursor()
cur.execute("SELECT last_topup FROM users WHERE user_id=?", (user_id,))
row = cur.fetchone(); conn.close()
last = row[0] if row else None
if not last: return True, 0
try: last_dt = datetime.fromisoformat(last)
except Exception: return True, 0
now = datetime.utcnow()
wait = timedelta(minutes=cooldown_minutes)
if now - last_dt >= wait: return True, 0
left = int((wait - (now - last_dt)).total_seconds())
return False, left
def claim_topup(user_id: int, amount: int):
ensure_user(user_id)
now_str = datetime.utcnow().isoformat(timespec="seconds")
conn = _connect(); cur = conn.cursor()
cur.execute("UPDATE users SET cash = cash + ?, last_topup = ? WHERE user_id = ?", (amount, now_str, user_id))
conn.commit(); conn.close()
def daily_cooldown(user_id: int, hours: int = 24) -> tuple[bool, int]:
ensure_user(user_id)
conn = _connect(); cur = conn.cursor()
cur.execute("SELECT last_daily_ts FROM users WHERE user_id=?", (user_id,))
row = cur.fetchone(); conn.close()
last = row[0] if row else None
if not last: return True, 0
try: last_dt = datetime.fromisoformat(last)
except Exception: return True, 0
now = datetime.utcnow()
wait = timedelta(hours=hours)
if now - last_dt >= wait: return True, 0
secs_left = int((wait - (now - last_dt)).total_seconds())
return False, secs_left
def claim_daily(user_id: int, cash_bonus: int, spin_bonus: int):
ensure_user(user_id)
today = str(date.today())
now_str = datetime.utcnow().isoformat(timespec="seconds")
conn = _connect(); cur = conn.cursor()
cur.execute("""
UPDATE users
SET cash = cash + ?, free_spins = free_spins + ?, last_daily = ?, last_daily_ts = ?
WHERE user_id = ?
""", (cash_bonus, spin_bonus, today, now_str, user_id))
conn.commit(); conn.close()
def transfer_cash(from_user_id: int, to_user_id: int, amount: int):
ensure_user(from_user_id); ensure_user(to_user_id)
if amount is None or amount <= 0: return False, "INVALID_AMOUNT", None, None
if from_user_id == to_user_id: return False, "SAME_USER", None, None
conn = _connect(); cur = conn.cursor()
try:
cur.execute("BEGIN IMMEDIATE")
cur.execute("SELECT cash FROM users WHERE user_id=?", (from_user_id,))
row = cur.fetchone()
if not row: conn.rollback(); return False, "INSUFFICIENT_FUNDS", None, None
from_cash = row[0]
if from_cash < amount: conn.rollback(); return False, "INSUFFICIENT_FUNDS", from_cash, None
cur.execute("UPDATE users SET cash = cash - ? WHERE user_id=?", (amount, from_user_id))
cur.execute("UPDATE users SET cash = cash + ? WHERE user_id=?", (amount, to_user_id))
cur.execute("SELECT cash FROM users WHERE user_id=?", (from_user_id,)); new_from = cur.fetchone()[0]
cur.execute("SELECT cash FROM users WHERE user_id=?", (to_user_id,)); new_to = cur.fetchone()[0]
conn.commit(); return True, None, new_from, new_to
except Exception:
conn.rollback(); raise
finally:
conn.close()

0
src/utils/__init__.py Normal file
View File

67
src/utils/constants.py Normal file
View File

@@ -0,0 +1,67 @@
import os
# Daily rewards
DAILY_CASH = int(os.getenv("DAILY_BONUS", "10000"))
DAILY_COOLDOWN_HOURS = int(os.getenv("DAILY_COOLDOWN_HOURS", "24"))
DAILY_FREE_SPINS = int(os.getenv("DAILY_FREE_SPINS", "10"))
# Slots base (now we support variable bets; this acts as default if user omits)
SLOTS_DEFAULT_BET = int(os.getenv("SLOTS_DEFAULT_BET", "8"))
SLOTS_MIN_BET = int(os.getenv("SLOTS_MIN_BET", "8"))
SLOTS_MAX_BET = int(os.getenv("SLOTS_MAX_BET", "5000"))
# Symbols and payouts (multiplier for three-in-a-line)
SYMBOLS = ["🍒", "🍋", "🍇", "🍉", "🍊", "", "💎"]
WEIGHTS = [6, 5, 4, 3, 3, 1, 1] # 🍒 🍋 🍇 🍉 🍊 ⭐ 💎
# Retuned payouts (pure 3-of-a-kind)
PAYOUTS = {"🍒":6, "🍋":12, "🍇":18, "🍉":26, "🍊":40, "":2200, "💎":2800}
# Wildcard nerf
WILDCARD_FACTOR = float(os.getenv("WILDCARD_FACTOR", "0.45"))
# Special combo: ⭐⭐⭐ on the middle row awards free spins
SPECIAL_SYMBOL = ""
SPECIAL_BONUS_SPINS = int(os.getenv("SLOTS_SPECIAL_BONUS_SPINS", "15"))
# Top-up faucet
TOPUP_AMOUNT = int(os.getenv("TOPUP_AMOUNT", "100"))
TOPUP_COOLDOWN_MINUTES = int(os.getenv("TOPUP_COOLDOWN_MINUTES", "5"))
# --- Roulette constants (Mini wheel: 018) ---
ROULETTE_MIN_CHIP = 10
ROULETTE_MIN_BET = 10
ROULETTE_MAX_BET = 100000
# Mini wheel pockets
ROULETTE_NUMBERS = list(range(19)) # 0..18
# Colors on the mini layout (subset of the standard pattern)
ROULETTE_RED = {1,3,5,7,9,12,14,16,18}
ROULETTE_BLACK = set(n for n in range(1, 19) if n not in ROULETTE_RED)
# Mini payouts (N:1). These correspond to (18/K - 1):1 where K is numbers covered.
ROULETTE_PAYOUTS = {
"straight": 17, # K=1
"split": 8, # K=2
"street": 5, # K=3 (incl. 0-1-2 and 0-2-3)
"sixline": 2, # K=6
"column": 2, # K=6
"sixes": 2, # groups 16 / 712 / 1318 (K=6)
"redblack": 1, # K=9 (0 loses)
"evenodd": 1, # K=9 (0 loses)
"lowhigh": 1, # K=9 (0 loses)
}
# --- Coin Flip constants ---
COINFLIP_MIN_CHIP = int(os.getenv("COINFLIP_MIN_CHIP", 10))
COINFLIP_MIN_BET = int(os.getenv("COINFLIP_MIN_BET", COINFLIP_MIN_CHIP))
COINFLIP_MAX_BET = int(os.getenv("COINFLIP_MAX_BET", 50000))
# --- Towers constants ---
TOWERS_MIN_BET = int(os.getenv("TOWERS_MIN_BET", 10))
TOWERS_MAX_BET = int(os.getenv("TOWERS_MAX_BET", 50000))
TOWERS_EDGE_PER_STEP = float(os.getenv("TOWERS_EDGE_PER_STEP", 0.02))