Update main.py
Browse files
main.py
CHANGED
|
@@ -7,6 +7,7 @@ import time
|
|
| 7 |
import json
|
| 8 |
import aiosqlite
|
| 9 |
import uvicorn
|
|
|
|
| 10 |
from contextlib import asynccontextmanager
|
| 11 |
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
| 12 |
from fastapi.middleware.cors import CORSMiddleware
|
|
@@ -30,7 +31,7 @@ WEBAPP_URL = f"https://{SPACE_HOST}"
|
|
| 30 |
|
| 31 |
PORT = int(os.getenv("PORT", 7860))
|
| 32 |
DB_PATH = "gift_bot.db"
|
| 33 |
-
HOUSE_EDGE = 0.
|
| 34 |
BUYBACK_RATE = 0.95
|
| 35 |
|
| 36 |
# ==========================================
|
|
@@ -124,7 +125,7 @@ async def refresh_prices():
|
|
| 124 |
return cached_prices
|
| 125 |
|
| 126 |
# ==========================================
|
| 127 |
-
# GAME ENGINE
|
| 128 |
# ==========================================
|
| 129 |
class CrashEngine:
|
| 130 |
def __init__(self):
|
|
@@ -141,27 +142,29 @@ class CrashEngine:
|
|
| 141 |
self.server_seed = secrets.token_hex(32)
|
| 142 |
self.server_seed_hash = hashlib.sha256(self.server_seed.encode()).hexdigest()
|
| 143 |
|
| 144 |
-
def calculate_crash(self
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
crash
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
| 152 |
|
| 153 |
def get_current_multiplier(self) -> float:
|
| 154 |
if not self.is_running: return 1.0
|
| 155 |
elapsed = time.time() - self.start_time
|
| 156 |
|
| 157 |
-
#
|
| 158 |
-
|
| 159 |
-
mult = 1.00 * (1.035 ** (elapsed * 10))
|
| 160 |
return round(mult, 2)
|
| 161 |
|
| 162 |
async def start_countdown(self):
|
| 163 |
self.generate_seed()
|
| 164 |
-
self.crash_point = self.calculate_crash(
|
| 165 |
self.is_countdown = True
|
| 166 |
self.is_running = False
|
| 167 |
self.bets = {}
|
|
@@ -193,7 +196,6 @@ class CrashEngine:
|
|
| 193 |
asyncio.create_task(self.start_countdown())
|
| 194 |
|
| 195 |
async def place_bet(self, user_id, amount):
|
| 196 |
-
# You can only place a bet during countdown or the first second
|
| 197 |
if not self.is_countdown and not self.is_running:
|
| 198 |
return False, "Game is not accepting bets."
|
| 199 |
if user_id in self.bets: return False, "Already bet this round."
|
|
@@ -223,7 +225,7 @@ class CrashEngine:
|
|
| 223 |
engine = CrashEngine()
|
| 224 |
|
| 225 |
# ==========================================
|
| 226 |
-
# TELEGRAM BOT
|
| 227 |
# ==========================================
|
| 228 |
bot = Bot(token=BOT_TOKEN) if BOT_TOKEN else None
|
| 229 |
dp = Dispatcher()
|
|
@@ -254,19 +256,21 @@ async def cmd_start(msg: types.Message):
|
|
| 254 |
await msg.answer(welcome_text, reply_markup=reply_kb)
|
| 255 |
await msg.answer("اضغط للدخول:", reply_markup=inline_kb)
|
| 256 |
|
| 257 |
-
|
| 258 |
-
async def open_game_msg(msg: types.Message):
|
| 259 |
-
await msg.answer("جاري فتح اللعبة...")
|
| 260 |
-
|
| 261 |
-
async def run_bot():
|
| 262 |
if not bot:
|
| 263 |
-
print("[ERROR] Bot token missing.
|
| 264 |
return
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
|
| 271 |
# ==========================================
|
| 272 |
# FASTAPI & WEBSOCKETS
|
|
@@ -276,7 +280,9 @@ async def lifespan(app: FastAPI):
|
|
| 276 |
await init_db()
|
| 277 |
await refresh_prices()
|
| 278 |
asyncio.create_task(engine.start_countdown())
|
| 279 |
-
|
|
|
|
|
|
|
| 280 |
async def price_loop():
|
| 281 |
while True:
|
| 282 |
await asyncio.sleep(300)
|
|
@@ -302,12 +308,12 @@ async def serve_frontend():
|
|
| 302 |
body { background: #0f0f0f; color: #fff; font-family: -apple-system, sans-serif; padding: 16px; min-height: 100vh; }
|
| 303 |
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
| 304 |
.balance { background: #1e1e1e; padding: 8px 16px; border-radius: 12px; font-weight: 600; }
|
| 305 |
-
.display { text-align: center; padding: 60px 0; background: #1a1a1a; border-radius: 24px; margin-bottom: 16px; position: relative; overflow: hidden; }
|
| 306 |
.display h1 { font-size: 4rem; font-weight: 800; letter-spacing: -2px; display: flex; align-items: center; justify-content: center; gap: 10px; }
|
| 307 |
.display.crashed h1 { color: #ff3b30; }
|
| 308 |
.display.crashed { background: #2a0a0a; }
|
| 309 |
.display.countdown { background: #1a1a2a; }
|
| 310 |
-
.display.countdown h1 { color: #007aff; }
|
| 311 |
.controls { display: flex; flex-direction: column; gap: 10px; margin-bottom: 20px; }
|
| 312 |
.row { display: flex; gap: 10px; }
|
| 313 |
input { flex: 1; padding: 16px; border-radius: 12px; border: none; background: #1e1e1e; color: #fff; font-size: 1.2rem; text-align: center; }
|
|
@@ -374,7 +380,6 @@ async def serve_frontend():
|
|
| 374 |
renderGifts(data.inventory);
|
| 375 |
}
|
| 376 |
else if (data.type === "countdown") {
|
| 377 |
-
// Show countdown state
|
| 378 |
displayEl.classList.remove("crashed");
|
| 379 |
displayEl.classList.add("countdown");
|
| 380 |
multEl.innerText = `Starting in ${data.seconds}...`;
|
|
@@ -387,12 +392,15 @@ async def serve_frontend():
|
|
| 387 |
cashBtn.disabled = true;
|
| 388 |
}
|
| 389 |
else if (data.type === "round_start") {
|
| 390 |
-
// Hide countdown, start flying
|
| 391 |
displayEl.classList.remove("countdown");
|
| 392 |
displayEl.classList.remove("crashed");
|
| 393 |
multEl.innerText = "🚀 1.00x";
|
| 394 |
isCountdown = false;
|
| 395 |
isRunning = true;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
}
|
| 397 |
else if (data.type === "tick") {
|
| 398 |
if(isRunning) {
|
|
@@ -411,7 +419,9 @@ async def serve_frontend():
|
|
| 411 |
hasBet = true;
|
| 412 |
betBtn.innerText = "Bet Placed";
|
| 413 |
betBtn.disabled = true;
|
|
|
|
| 414 |
if(isRunning) cashBtn.disabled = false;
|
|
|
|
| 415 |
const amt = parseFloat(document.getElementById('betAmount').value);
|
| 416 |
balanceEl.innerText = (parseFloat(balanceEl.innerText) - amt).toFixed(2);
|
| 417 |
} else alert(data.msg);
|
|
|
|
| 7 |
import json
|
| 8 |
import aiosqlite
|
| 9 |
import uvicorn
|
| 10 |
+
import threading
|
| 11 |
from contextlib import asynccontextmanager
|
| 12 |
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
| 13 |
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
| 31 |
|
| 32 |
PORT = int(os.getenv("PORT", 7860))
|
| 33 |
DB_PATH = "gift_bot.db"
|
| 34 |
+
HOUSE_EDGE = 0.04 # 4% house edge
|
| 35 |
BUYBACK_RATE = 0.95
|
| 36 |
|
| 37 |
# ==========================================
|
|
|
|
| 125 |
return cached_prices
|
| 126 |
|
| 127 |
# ==========================================
|
| 128 |
+
# GAME ENGINE (FIXED ALGORITHM)
|
| 129 |
# ==========================================
|
| 130 |
class CrashEngine:
|
| 131 |
def __init__(self):
|
|
|
|
| 142 |
self.server_seed = secrets.token_hex(32)
|
| 143 |
self.server_seed_hash = hashlib.sha256(self.server_seed.encode()).hexdigest()
|
| 144 |
|
| 145 |
+
def calculate_crash(self) -> float:
|
| 146 |
+
# Provably fair formula (generates up to 1000x+)
|
| 147 |
+
r = secrets.randbelow(2**32) / (2**32)
|
| 148 |
+
# House edge cut
|
| 149 |
+
crash = (1 - HOUSE_EDGE) / (1 - r)
|
| 150 |
+
|
| 151 |
+
if crash < 1.0:
|
| 152 |
+
crash = 1.0
|
| 153 |
+
|
| 154 |
+
# Cap the max multiplier for sanity
|
| 155 |
+
return min(round(crash, 2), 100.0)
|
| 156 |
|
| 157 |
def get_current_multiplier(self) -> float:
|
| 158 |
if not self.is_running: return 1.0
|
| 159 |
elapsed = time.time() - self.start_time
|
| 160 |
|
| 161 |
+
# Slower growth curve: 2x takes ~5 seconds, 10x takes ~13 seconds.
|
| 162 |
+
mult = 1.00 * (1.05 ** (elapsed * 2.5))
|
|
|
|
| 163 |
return round(mult, 2)
|
| 164 |
|
| 165 |
async def start_countdown(self):
|
| 166 |
self.generate_seed()
|
| 167 |
+
self.crash_point = self.calculate_crash()
|
| 168 |
self.is_countdown = True
|
| 169 |
self.is_running = False
|
| 170 |
self.bets = {}
|
|
|
|
| 196 |
asyncio.create_task(self.start_countdown())
|
| 197 |
|
| 198 |
async def place_bet(self, user_id, amount):
|
|
|
|
| 199 |
if not self.is_countdown and not self.is_running:
|
| 200 |
return False, "Game is not accepting bets."
|
| 201 |
if user_id in self.bets: return False, "Already bet this round."
|
|
|
|
| 225 |
engine = CrashEngine()
|
| 226 |
|
| 227 |
# ==========================================
|
| 228 |
+
# TELEGRAM BOT (ISOLATED THREAD FIX)
|
| 229 |
# ==========================================
|
| 230 |
bot = Bot(token=BOT_TOKEN) if BOT_TOKEN else None
|
| 231 |
dp = Dispatcher()
|
|
|
|
| 256 |
await msg.answer(welcome_text, reply_markup=reply_kb)
|
| 257 |
await msg.answer("اضغط للدخول:", reply_markup=inline_kb)
|
| 258 |
|
| 259 |
+
def start_bot_in_thread():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
if not bot:
|
| 261 |
+
print("[ERROR] Bot token missing.")
|
| 262 |
return
|
| 263 |
+
print("[BOT] Starting Telegram bot polling in background...")
|
| 264 |
+
|
| 265 |
+
async def run():
|
| 266 |
+
try:
|
| 267 |
+
await dp.start_polling(bot)
|
| 268 |
+
except Exception as e:
|
| 269 |
+
print(f"[BOT ERROR] {e}")
|
| 270 |
+
|
| 271 |
+
loop = asyncio.new_event_loop()
|
| 272 |
+
asyncio.set_event_loop(loop)
|
| 273 |
+
loop.run_until_complete(run())
|
| 274 |
|
| 275 |
# ==========================================
|
| 276 |
# FASTAPI & WEBSOCKETS
|
|
|
|
| 280 |
await init_db()
|
| 281 |
await refresh_prices()
|
| 282 |
asyncio.create_task(engine.start_countdown())
|
| 283 |
+
# Start bot in a separate thread to prevent blocking FastAPI startup
|
| 284 |
+
threading.Thread(target=start_bot_in_thread, daemon=True).start()
|
| 285 |
+
|
| 286 |
async def price_loop():
|
| 287 |
while True:
|
| 288 |
await asyncio.sleep(300)
|
|
|
|
| 308 |
body { background: #0f0f0f; color: #fff; font-family: -apple-system, sans-serif; padding: 16px; min-height: 100vh; }
|
| 309 |
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
| 310 |
.balance { background: #1e1e1e; padding: 8px 16px; border-radius: 12px; font-weight: 600; }
|
| 311 |
+
.display { text-align: center; padding: 60px 0; background: #1a1a1a; border-radius: 24px; margin-bottom: 16px; position: relative; overflow: hidden; transition: background 0.3s; }
|
| 312 |
.display h1 { font-size: 4rem; font-weight: 800; letter-spacing: -2px; display: flex; align-items: center; justify-content: center; gap: 10px; }
|
| 313 |
.display.crashed h1 { color: #ff3b30; }
|
| 314 |
.display.crashed { background: #2a0a0a; }
|
| 315 |
.display.countdown { background: #1a1a2a; }
|
| 316 |
+
.display.countdown h1 { color: #007aff; font-size: 2.5rem; }
|
| 317 |
.controls { display: flex; flex-direction: column; gap: 10px; margin-bottom: 20px; }
|
| 318 |
.row { display: flex; gap: 10px; }
|
| 319 |
input { flex: 1; padding: 16px; border-radius: 12px; border: none; background: #1e1e1e; color: #fff; font-size: 1.2rem; text-align: center; }
|
|
|
|
| 380 |
renderGifts(data.inventory);
|
| 381 |
}
|
| 382 |
else if (data.type === "countdown") {
|
|
|
|
| 383 |
displayEl.classList.remove("crashed");
|
| 384 |
displayEl.classList.add("countdown");
|
| 385 |
multEl.innerText = `Starting in ${data.seconds}...`;
|
|
|
|
| 392 |
cashBtn.disabled = true;
|
| 393 |
}
|
| 394 |
else if (data.type === "round_start") {
|
|
|
|
| 395 |
displayEl.classList.remove("countdown");
|
| 396 |
displayEl.classList.remove("crashed");
|
| 397 |
multEl.innerText = "🚀 1.00x";
|
| 398 |
isCountdown = false;
|
| 399 |
isRunning = true;
|
| 400 |
+
// CRITICAL FIX: If player already bet during countdown, unlock cashout
|
| 401 |
+
if (hasBet) {
|
| 402 |
+
cashBtn.disabled = false;
|
| 403 |
+
}
|
| 404 |
}
|
| 405 |
else if (data.type === "tick") {
|
| 406 |
if(isRunning) {
|
|
|
|
| 419 |
hasBet = true;
|
| 420 |
betBtn.innerText = "Bet Placed";
|
| 421 |
betBtn.disabled = true;
|
| 422 |
+
// CRITICAL FIX: If the round is already running, let them cashout immediately
|
| 423 |
if(isRunning) cashBtn.disabled = false;
|
| 424 |
+
|
| 425 |
const amt = parseFloat(document.getElementById('betAmount').value);
|
| 426 |
balanceEl.innerText = (parseFloat(balanceEl.innerText) - amt).toFixed(2);
|
| 427 |
} else alert(data.msg);
|