nextgen45/cricket-auction
1
1import os2import sys3sys.stderr.write("๐ [BACKEND] PYTHON PROCESS STARTING UP...\n")4sys.stderr.flush()5import json6import time7import random8import string9import threading10import queue11import base6412import hashlib13import uuid14from urllib.parse import urlparse, parse_qs15import sqlite316from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer17 18 19# Global room state store and lock20rooms = {}21rooms_lock = threading.Lock()22 23# Cricket Player Presets24PRESETS = {25 "ipl_legends": [26 {"id": 1, "name": "Virat Kohli", "role": "Batsman", "rating": 96, "base_price": 20000000, "stats": "Runs: 7624, Avg: 38.7, SR: 130.7", "img": "https://img1.hscicdn.com/image/upload/f_auto,t_ds_square_w_320,q_50/lsci/db/PICTURES/CMS/316600/316605.png", "overseas": False},27 {"id": 2, "name": "MS Dhoni", "role": "Wicket-Keeper", "rating": 95, "base_price": 20000000, "stats": "Runs: 5243, Avg: 39.1, SR: 137.5, Catch/Stump: 192", "img": "https://img1.hscicdn.com/image/upload/f_auto,t_ds_square_w_320,q_50/lsci/db/PICTURES/CMS/319900/319946.png", "overseas": False},28 {"id": 3, "name": "Jasprit Bumrah", "role": "Bowler", "rating": 98, "base_price": 20000000, "stats": "Wickets: 165, Econ: 7.30, Avg: 22.5", "img": "https://img1.hscicdn.com/image/upload/f_auto,t_ds_square_w_320,q_50/lsci/db/PICTURES/CMS/316500/316584.png", "overseas": False},29 {"id": 4, "name": "Rohit Sharma", "role": "Batsman", "rating": 94, "base_price": 20000000, "stats": "Runs: 6628, Avg: 29.7, SR: 131.2", "img": "https://img1.hscicdn.com/image/upload/f_auto,t_ds_square_w_320,q_50/lsci/db/PICTURES/CMS/316500/316581.png", "overseas": False},30 {"id": 5, "name": "Ravindra Jadeja", "role": "All-Rounder", "rating": 93, "base_price": 15000000, "stats": "Runs: 2958, SR: 129.5, Wickets: 160, Econ: 7.62", "img": "https://img1.hscicdn.com/image/upload/f_auto,t_ds_square_w_320,q_50/lsci/db/PICTURES/CMS/316500/316534.png", "overseas": False},31 {"id": 6, "name": "Hardik Pandya", "role": "All-Rounder", "rating": 92, "base_price": 15000000, "stats": "Runs: 2525, SR: 145.8, Wickets: 64, Econ: 8.75", "img": "https://img1.hscicdn.com/image/upload/f_auto,t_ds_square_w_320,q_50/lsci/db/PICTURES/CMS/316500/316587.png", "overseas": False},32 {"id": 7, "name": "Heinrich Klaasen", "role": "Wicket-Keeper", "rating": 94, "base_price": 15000000, "stats": "Runs: 980, Avg: 41.2, SR: 168.3", "img": "https://img1.hscicdn.com/image/upload/f_auto,t_ds_square_w_320,q_50/lsci/db/PICTURES/CMS/319000/319056.png", "overseas": True},33 {"id": 8, "name": "Rashid Khan", "role": "Bowler", "rating": 95, "base_price": 15000000, "stats": "Wickets: 148, Econ: 6.78, Avg: 20.8", "img": "https://img1.hscicdn.com/image/upload/f_auto,t_ds_square_w_320,q_50/lsci/db/PICTURES/CMS/313300/313398.png", "overseas": True},34 {"id": 9, "name": "Suryakumar Yadav", "role": "Batsman", "rating": 95, "base_price": 15000000, "stats": "Runs: 3594, Avg: 32.4, SR: 143.6", "img": "https://img1.hscicdn.com/image/upload/f_auto,t_ds_square_w_320,q_50/lsci/db/PICTURES/CMS/316600/316620.png", "overseas": False},35 {"id": 10, "name": "Travis Head", "role": "Batsman", "rating": 93, "base_price": 15000000, "stats": "Runs: 960, Avg: 36.8, SR: 172.5", "img": "https://img1.hscicdn.com/image/upload/f_auto,t_ds_square_w_320,q_50/lsci/db/PICTURES/CMS/319000/319001.png", "overseas": True},36 {"id": 11, "name": "Rishabh Pant", "role": "Wicket-Keeper", "rating": 91, "base_price": 15000000, "stats": "Runs: 3284, Avg: 35.3, SR: 148.9", "img": "https://img1.hscicdn.com/image/upload/f_auto,t_ds_square_w_320,q_50/lsci/db/PICTURES/CMS/316600/316612.png", "overseas": False},37 {"id": 12, "name": "Sunil Narine", "role": "All-Rounder", "rating": 94, "base_price": 15000000, "stats": "Runs: 1540, SR: 162.4, Wickets: 180, Econ: 6.64", "img": "https://img1.hscicdn.com/image/upload/f_auto,t_ds_square_w_320,q_50/lsci/db/PICTURES/CMS/319000/319030.png", "overseas": True},38 {"id": 13, "name": "Mitchell Starc", "role": "Bowler", "rating": 92, "base_price": 20000000, "stats": "Wickets: 95, Econ: 8.12, Avg: 24.6", "img": "https://img1.hscicdn.com/image/upload/f_auto,t_ds_square_w_320,q_50/lsci/db/PICTURES/CMS/319000/319052.png", "overseas": True},39 {"id": 14, "name": "Yuzvendra Chahal", "role": "Bowler", "rating": 90, "base_price": 10000000, "stats": "Wickets: 205, Econ: 7.84, Avg: 22.4", "img": "https://img1.hscicdn.com/image/upload/f_auto,t_ds_square_w_320,q_50/lsci/db/PICTURES/CMS/316600/316618.png", "overseas": False},40 {"id": 15, "name": "Andre Russell", "role": "All-Rounder", "rating": 93, "base_price": 15000000, "stats": "Runs: 2484, SR: 174.0, Wickets: 115, Econ: 9.18", "img": "https://img1.hscicdn.com/image/upload/f_auto,t_ds_square_w_320,q_50/lsci/db/PICTURES/CMS/319000/319028.png", "overseas": True}41 ],42 "all_time_legends": [43 {"id": 101, "name": "Sachin Tendulkar", "role": "Batsman", "rating": 99, "base_price": 20000000, "stats": "Runs: 18426, Avg: 44.8, SR: 86.2 (ODI)", "img": "https://img1.hscicdn.com/image/upload/f_auto,t_ds_square_w_320,q_50/lsci/db/PICTURES/CMS/316500/316599.png", "overseas": False},44 {"id": 102, "name": "Viv Richards", "role": "Batsman", "rating": 98, "base_price": 20000000, "stats": "Runs: 6721, Avg: 47.0, SR: 90.2 (ODI)", "img": "https://img1.hscicdn.com/image/upload/f_auto,t_ds_square_w_320,q_50/lsci/db/PICTURES/CMS/319000/319043.png", "overseas": True},45 {"id": 103, "name": "Shane Warne", "role": "Bowler", "rating": 99, "base_price": 20000000, "stats": "Wickets: 708 (Test), Wickets: 293 (ODI)", "img": "https://img1.hscicdn.com/image/upload/f_auto,t_ds_square_w_320,q_50/lsci/db/PICTURES/CMS/319000/319045.png", "overseas": True},46 {"id": 104, "name": "Wasim Akram", "role": "Bowler", "rating": 98, "base_price": 20000000, "stats": "Wickets: 502 (ODI), Econ: 3.90, Wickets: 414 (Test)", "img": "https://img1.hscicdn.com/image/upload/f_auto,t_ds_square_w_320,q_50/lsci/db/PICTURES/CMS/319000/319048.png", "overseas": True},47 {"id": 105, "name": "Jacques Kallis", "role": "All-Rounder", "rating": 98, "base_price": 20000000, "stats": "Runs: 11579 (ODI), Wickets: 273 (ODI)", "img": "https://img1.hscicdn.com/image/upload/f_auto,t_ds_square_w_320,q_50/lsci/db/PICTURES/CMS/319000/319050.png", "overseas": True},48 {"id": 106, "name": "Adam Gilchrist", "role": "Wicket-Keeper", "rating": 97, "base_price": 20000000, "stats": "Runs: 9619, Avg: 35.8, SR: 96.9, Dismissals: 472", "img": "https://img1.hscicdn.com/image/upload/f_auto,t_ds_square_w_320,q_50/lsci/db/PICTURES/CMS/319000/319054.png", "overseas": True},49 {"id": 107, "name": "AB de Villiers", "role": "Batsman", "rating": 98, "base_price": 20000000, "stats": "Runs: 9577, Avg: 53.5, SR: 101.2, Fast ODI 100 (31b)", "img": "https://img1.hscicdn.com/image/upload/f_auto,t_ds_square_w_320,q_50/lsci/db/PICTURES/CMS/319000/319055.png", "overseas": True},50 {"id": 108, "name": "Glenn McGrath", "role": "Bowler", "rating": 97, "base_price": 15000000, "stats": "Wickets: 563 (Test), Econ: 2.49, Wickets: 381 (ODI)", "img": "https://img1.hscicdn.com/image/upload/f_auto,t_ds_square_w_320,q_50/lsci/db/PICTURES/CMS/319000/319058.png", "overseas": True}51 ]52}53 54# Load players database from public/players.json if it exists55players_json_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'public', 'players.json')56if os.path.exists(players_json_path):57 try:58 with open(players_json_path, 'r', encoding='utf-8') as f:59 loaded_presets = json.load(f)60 PRESETS.update(loaded_presets)61 print(f"Loaded players dynamically. Pools: {list(PRESETS.keys())}, Full Pool contains {len(PRESETS.get('full_pool', []))} players.")62 except Exception as e:63 print(f"Error loading players.json: {e}")64 65# User Account Registry Database66# Make users database persistent on local Windows, fallback to workspace for Render/Linux hosting67if os.name == 'nt':68 persistent_dir = os.path.join(os.environ.get('USERPROFILE', 'C:\\Users\\Chaitanya'), '.cricket_auction')69else:70 persistent_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data')71 72if not os.path.exists(persistent_dir):73 try:74 os.makedirs(persistent_dir)75 except Exception as e:76 sys.stderr.write(f"โ ๏ธ Error creating persistent directory {persistent_dir}: {e}. Falling back to /tmp/.cricket_auction\n")77 sys.stderr.flush()78 persistent_dir = "/tmp/.cricket_auction"79 if not os.path.exists(persistent_dir):80 try:81 os.makedirs(persistent_dir)82 except Exception as ex:83 sys.stderr.write(f"โ Failed to create /tmp fallback directory: {ex}\n")84 sys.stderr.flush()85 86def get_serializable_room(room):87 """Deep copies room state except clients list which has Queue objects."""88 serializable = {}89 for k, v in room.items():90 if k != "clients":91 serializable[k] = v92 return serializable93 94# Database class supporting local SQLite and cloud PostgreSQL95class AuctionDB:96 def __init__(self):97 self.db_url = os.environ.get('DATABASE_URL')98 self.is_postgres = False99 100 if self.db_url and (self.db_url.startswith('postgres://') or self.db_url.startswith('postgresql://')):101 try:102 import psycopg2103 self.is_postgres = True104 sys.stderr.write("๐ Connecting to cloud PostgreSQL database...\n")105 sys.stderr.flush()106 except ImportError:107 sys.stderr.write("โ ๏ธ Warning: DATABASE_URL is set but 'psycopg2' is not installed. Falling back to local SQLite.\n")108 sys.stderr.flush()109 110 if not self.is_postgres:111 self.sqlite_path = os.path.join(persistent_dir, 'auction.db')112 sys.stderr.write(f"๐ Connecting to local SQLite database at: {self.sqlite_path}\n")113 sys.stderr.flush()114 115 self.init_db()116 self.migrate_json_data()117 118 def migrate_json_data(self):119 # 1. Migrate users.json120 users_json = os.path.join(persistent_dir, 'users.json')121 if os.path.exists(users_json):122 try:123 print("Migrating users.json to SQL database...")124 with open(users_json, 'r', encoding='utf-8') as f:125 old_users = json.load(f)126 127 conn = self.get_connection()128 cursor = conn.cursor()129 migrated = 0130 for username, info in old_users.items():131 password_hash = info.get("password_hash")132 salt = info.get("salt")133 if password_hash and salt:134 if self.is_postgres:135 cursor.execute(136 "INSERT INTO users (username, password_hash, salt) VALUES (%s, %s, %s) ON CONFLICT (username) DO NOTHING",137 (username, password_hash, salt)138 )139 else:140 cursor.execute(141 "INSERT OR IGNORE INTO users (username, password_hash, salt) VALUES (?, ?, ?)",142 (username, password_hash, salt)143 )144 migrated += 1145 conn.commit()146 conn.close()147 print(f"Migrated {migrated} users to database.")148 os.rename(users_json, users_json + ".bak")149 except Exception as e:150 print(f"Error migrating users.json: {e}")151 152 # 2. Migrate rooms/*.json and *.json.bak153 rooms_dir = os.path.join(persistent_dir, 'rooms')154 if os.path.exists(rooms_dir):155 try:156 files = os.listdir(rooms_dir)157 room_files = [f for f in files if f.endswith('.json') or f.endswith('.json.bak')]158 if room_files:159 print(f"Migrating {len(room_files)} room JSON files to SQL database...")160 existing_rooms = self.load_rooms()161 for file in room_files:162 room_code = file[:4].upper()163 if room_code in existing_rooms:164 if file.endswith('.json'):165 try:166 os.rename(os.path.join(rooms_dir, file), os.path.join(rooms_dir, room_code + '.json.bak'))167 except Exception:168 pass169 continue170 171 room_file = os.path.join(rooms_dir, file)172 try:173 with open(room_file, 'r', encoding='utf-8') as f:174 room_data = json.load(f)175 self.save_room(room_code, room_data)176 if file.endswith('.json'):177 os.rename(room_file, room_file + ".bak")178 except Exception as ex:179 print(f"Error migrating room file {file}: {ex}")180 print("Rooms migration complete.")181 except Exception as e:182 print(f"Error migrating room files: {e}")183 184 def get_connection(self):185 if self.is_postgres:186 try:187 import psycopg2188 url = self.db_url189 if url.startswith('postgres://'):190 url = url.replace('postgres://', 'postgresql://', 1)191 return psycopg2.connect(url)192 except Exception as e:193 sys.stderr.write(f"โ PostgreSQL connection failed: {e}. Falling back to SQLite.\n")194 sys.stderr.flush()195 self.is_postgres = False196 197 return sqlite3.connect(self.sqlite_path)198 199 def init_db(self):200 try:201 conn = self.get_connection()202 cursor = conn.cursor()203 if self.is_postgres:204 cursor.execute("""205 CREATE TABLE IF NOT EXISTS users (206 username VARCHAR(255) PRIMARY KEY,207 password_hash TEXT NOT NULL,208 salt TEXT NOT NULL209 );210 """)211 cursor.execute("""212 CREATE TABLE IF NOT EXISTS rooms (213 room_code VARCHAR(10) PRIMARY KEY,214 host_username VARCHAR(255) NOT NULL,215 created_at VARCHAR(50) NOT NULL,216 status VARCHAR(50) NOT NULL,217 state_json TEXT NOT NULL218 );219 """)220 else:221 cursor.execute("""222 CREATE TABLE IF NOT EXISTS users (223 username TEXT PRIMARY KEY,224 password_hash TEXT NOT NULL,225 salt TEXT NOT NULL226 );227 """)228 cursor.execute("""229 CREATE TABLE IF NOT EXISTS rooms (230 room_code TEXT PRIMARY KEY,231 host_username TEXT NOT NULL,232 created_at TEXT NOT NULL,233 status TEXT NOT NULL,234 state_json TEXT NOT NULL235 );236 """)237 conn.commit()238 conn.close()239 except Exception as e:240 sys.stderr.write(f"โ Database initialization error: {e}\n")241 sys.stderr.flush()242 243 def load_users(self):244 conn = self.get_connection()245 cursor = conn.cursor()246 users_dict = {}247 try:248 cursor.execute("SELECT username, password_hash, salt FROM users")249 for row in cursor.fetchall():250 users_dict[row[0]] = {251 "password_hash": row[1],252 "salt": row[2]253 }254 except Exception as e:255 print(f"Error loading users from database: {e}")256 finally:257 conn.close()258 return users_dict259 260 def save_user(self, username, password_hash, salt):261 conn = self.get_connection()262 cursor = conn.cursor()263 try:264 if self.is_postgres:265 cursor.execute(266 "INSERT INTO users (username, password_hash, salt) VALUES (%s, %s, %s) ON CONFLICT (username) DO UPDATE SET password_hash = EXCLUDED.password_hash, salt = EXCLUDED.salt",267 (username, password_hash, salt)268 )269 else:270 cursor.execute(271 "INSERT OR REPLACE INTO users (username, password_hash, salt) VALUES (?, ?, ?)",272 (username, password_hash, salt)273 )274 conn.commit()275 except Exception as e:276 print(f"Error saving user {username} to database: {e}")277 finally:278 conn.close()279 280 def load_rooms(self):281 conn = self.get_connection()282 cursor = conn.cursor()283 rooms_dict = {}284 try:285 cursor.execute("SELECT room_code, state_json FROM rooms")286 for row in cursor.fetchall():287 room_code = row[0].upper()288 try:289 room_data = json.loads(row[1])290 room_data["clients"] = []291 room_data["bot_delay"] = 0292 room_data["timer_active"] = False293 if "season" not in room_data:294 room_data["season"] = 1295 if "standings" not in room_data:296 room_data["standings"] = {}297 if "trades" not in room_data:298 room_data["trades"] = []299 if "retentions" not in room_data:300 room_data["retentions"] = {}301 if "retention_locked" not in room_data:302 room_data["retention_locked"] = {}303 if "trade_window_open" not in room_data:304 room_data["trade_window_open"] = False305 rooms_dict[room_code] = room_data306 except Exception as ex:307 print(f"Error parsing JSON for room {room_code}: {ex}")308 except Exception as e:309 print(f"Error loading rooms from database: {e}")310 finally:311 conn.close()312 return rooms_dict313 314 def save_room(self, room_code, room):315 conn = self.get_connection()316 cursor = conn.cursor()317 try:318 serializable = get_serializable_room(room)319 state_json = json.dumps(serializable)320 host_username = room.get("host_username", "")321 created_at = room.get("created_at", "")322 status = room.get("status", "lobby")323 324 if self.is_postgres:325 cursor.execute(326 "INSERT INTO rooms (room_code, host_username, created_at, status, state_json) VALUES (%s, %s, %s, %s, %s) ON CONFLICT (room_code) DO UPDATE SET status = EXCLUDED.status, state_json = EXCLUDED.state_json",327 (room_code, host_username, created_at, status, state_json)328 )329 else:330 cursor.execute(331 "INSERT OR REPLACE INTO rooms (room_code, host_username, created_at, status, state_json) VALUES (?, ?, ?, ?, ?)",332 (room_code, host_username, created_at, status, state_json)333 )334 conn.commit()335 except Exception as e:336 print(f"Error saving room {room_code} to database: {e}")337 finally:338 conn.close()339 340 def delete_room(self, room_code):341 conn = self.get_connection()342 cursor = conn.cursor()343 try:344 if self.is_postgres:345 cursor.execute("DELETE FROM rooms WHERE room_code = %s", (room_code,))346 else:347 cursor.execute("DELETE FROM rooms WHERE room_code = ?", (room_code,))348 conn.commit()349 except Exception as e:350 print(f"Error deleting room {room_code} from database: {e}")351 finally:352 conn.close()353 354db = AuctionDB()355users = db.load_users()356users_lock = threading.Lock()357print(f"Loaded {len(users)} registered user accounts from database.")358 359# Session Management (Token -> Username)360sessions = {}361sessions_lock = threading.Lock()362 363# Background Database Save Worker to serialize SQLite writes asynchronously364db_save_queue = queue.Queue()365 366def db_save_worker():367 while True:368 try:369 item = db_save_queue.get()370 if item is None:371 break372 room_code, state_json, host_username, created_at, status = item373 374 conn = db.get_connection()375 cursor = conn.cursor()376 try:377 if db.is_postgres:378 cursor.execute(379 "INSERT INTO rooms (room_code, host_username, created_at, status, state_json) VALUES (%s, %s, %s, %s, %s) ON CONFLICT (room_code) DO UPDATE SET status = EXCLUDED.status, state_json = EXCLUDED.state_json",380 (room_code, host_username, created_at, status, state_json)381 )382 else:383 cursor.execute(384 "INSERT OR REPLACE INTO rooms (room_code, host_username, created_at, status, state_json) VALUES (?, ?, ?, ?, ?)",385 (room_code, host_username, created_at, status, state_json)386 )387 conn.commit()388 except Exception as e:389 print(f"Error in background db save for room {room_code}: {e}")390 finally:391 conn.close()392 db_save_queue.task_done()393 except Exception as ex:394 print(f"Error in db_save_worker: {ex}")395 396# Start the background SQLite writer thread397save_thread = threading.Thread(target=db_save_worker, daemon=True)398save_thread.start()399 400def hash_password(password, salt=None):401 if salt is None:402 salt = os.urandom(16).hex()403 hashed = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt.encode('utf-8'), 100000).hex()404 return hashed, salt405 406def get_user_from_token(token):407 if not token:408 return None409 if token.startswith("guest_"):410 return "Guest_" + token[6:12]411 with sessions_lock:412 return sessions.get(token)413 414def create_session(username):415 token = "auth_" + uuid.uuid4().hex416 with sessions_lock:417 # Invalidate any other active sessions for this user to ensure 1 active ID/session at any time418 for t, u in list(sessions.items()):419 if u == username:420 del sessions[t]421 sessions[token] = username422 return token423 424def format_currency_python(val):425 if val >= 10000000:426 return f"{val / 10000000:.2f} Cr"427 elif val >= 100000:428 return f"{val / 100000:.2f} L"429 return f"{val:,}"430 431def save_room_to_disk(room_code):432 if room_code not in rooms:433 return434 room = rooms[room_code]435 serializable = get_serializable_room(room)436 state_json = json.dumps(serializable)437 host_username = room.get("host_username", "")438 created_at = room.get("created_at", "")439 status = room.get("status", "lobby")440 db_save_queue.put((room_code, state_json, host_username, created_at, status))441 442def load_all_rooms():443 global rooms444 rooms = db.load_rooms()445 print(f"Loaded {len(rooms)} persistent auction rooms from database.")446 447load_all_rooms()448 449def broadcast(room_code, event_type, data):450 """Pushes events to all client queues in a specific room."""451 if room_code not in rooms:452 return453 room = rooms[room_code]454 455 # Pre-serialize to JSON once to avoid redundant string serialization in each client thread456 data_json = json.dumps(data)457 payload = {458 "type": event_type,459 "data_json": data_json,460 "timestamp": time.time()461 }462 463 to_remove = []464 for client_id, q in room["clients"]:465 try:466 q.put(payload)467 except Exception:468 to_remove.append((client_id, q))469 470 for client in to_remove:471 if client in room["clients"]:472 room["clients"].remove(client)473 474def sell_current_player(room_code):475 room = rooms[room_code]476 idx = room["current_player_index"]477 if idx < 0 or idx >= len(room["players"]):478 return479 480 room["last_completed_player_index"] = idx481 if "completed_players_history" not in room:482 room["completed_players_history"] = []483 room["completed_players_history"].append(idx)484 player = room["players"][idx]485 bidder = room["current_bidder"]486 price = room["current_bid"]487 488 player["status"] = "sold"489 player["bought_by"] = bidder490 player["price"] = price491 492 # Deduct budget493 if bidder in room["teams"]:494 room["teams"][bidder]["budget"] -= price495 room["teams"][bidder]["players"].append({496 "id": player["id"],497 "name": player["name"],498 "role": player["role"],499 "price": price,500 "rating": player["rating"],501 "overseas": player.get("overseas", False)502 })503 504 role = player["role"]505 if role in room["teams"][bidder]["slots"]:506 room["teams"][bidder]["slots"][role] += 1507 508 log_msg = f"๐จ SOLD: {player['name']} bought by {bidder} for {format_currency_python(price)}!"509 room["logs"].append(log_msg)510 511 room["status"] = "sold_pause"512 room["timer_active"] = False513 if room["settings"].get("bot_auctioneer"):514 room["bot_delay"] = 3515 516 save_room_to_disk(room_code)517 broadcast(room_code, "player_sold", {518 "player": player,519 "bidder": bidder,520 "price": price,521 "log": log_msg,522 "room": get_serializable_room(room)523 })524 525def unsold_current_player(room_code):526 room = rooms[room_code]527 idx = room["current_player_index"]528 if idx < 0 or idx >= len(room["players"]):529 return530 531 room["last_completed_player_index"] = idx532 if "completed_players_history" not in room:533 room["completed_players_history"] = []534 room["completed_players_history"].append(idx)535 player = room["players"][idx]536 player["status"] = "passed"537 538 log_msg = f"โ UNSOLD: {player['name']} went unsold at base price of {format_currency_python(player['base_price'])}."539 room["logs"].append(log_msg)540 541 room["status"] = "unsold_pause"542 room["timer_active"] = False543 if room["settings"].get("bot_auctioneer"):544 room["bot_delay"] = 3545 546 save_room_to_disk(room_code)547 broadcast(room_code, "player_unsold", {548 "player": player,549 "log": log_msg,550 "room": get_serializable_room(room)551 })552 553def advance_to_next_player(room_code, role_filter="All", player_id=None):554 room = rooms[room_code]555 idx = room["current_player_index"]556 next_unsold = -1557 558 # Helper to check if player matches the active filter (role or unsold status)559 def matches_filter(p):560 if p.get("bought_by") is not None or p.get("status") == "sold":561 return False562 if role_filter == "Unsold":563 return p.get("status") == "passed"564 else:565 if p.get("status", "unsold") != "unsold":566 return False567 return role_filter == "All" or not role_filter or p.get("role") == role_filter568 569 # If a specific player is requested by the host570 if player_id is not None:571 try:572 player_id_int = int(player_id)573 for i, p in enumerate(room["players"]):574 if p["id"] == player_id_int:575 # Verify player is unsold and not bought576 if p.get("status", "unsold") != "sold" and p.get("bought_by") is None:577 next_unsold = i578 break579 except (ValueError, TypeError):580 pass581 582 # Fallback to sequential/filtered search if no specific player was found or requested583 if next_unsold == -1:584 # If starting for the first time585 if idx == -1:586 # Find first unsold player matching role_filter587 for i, p in enumerate(room["players"]):588 if matches_filter(p):589 next_unsold = i590 break591 action_msg = "๐ฌ Auction started!"592 else:593 # Search starting after current index594 for i in range(idx + 1, len(room["players"])):595 p = room["players"][i]596 if matches_filter(p):597 next_unsold = i598 break599 600 # Wrap around if not found601 if next_unsold == -1:602 for i in range(0, idx + 1):603 p = room["players"][i]604 if matches_filter(p):605 next_unsold = i606 break607 action_msg = "๐ Next up:"608 else:609 action_msg = "๐ฏ Host introduced:"610 611 if next_unsold == -1:612 if role_filter and role_filter != "All":613 return False, f"No unsold players left in category: {role_filter}!"614 else:615 room["status"] = "finished"616 finish_msg = "๐ Cricket Player Auction completed! All players auctioned."617 room["logs"].append(finish_msg)618 save_room_to_disk(room_code)619 return True, finish_msg620 else:621 room["current_player_index"] = next_unsold622 room["status"] = "active"623 room["current_bid"] = 0624 room["current_bidder"] = None625 room["timer"] = room["settings"]["timer_duration"]626 room["timer_active"] = True627 628 player = room["players"][next_unsold]629 player_name = player["name"]630 msg = f"{action_msg} {player_name} (Base: {format_currency_python(player['base_price'])})"631 room["logs"].append(msg)632 save_room_to_disk(room_code)633 return True, msg634 635# Global room countdown timer background thread636def timer_worker():637 while True:638 time.sleep(1)639 with rooms_lock:640 for room_code, room in list(rooms.items()):641 if room["status"] == "active" and room["timer_active"]:642 # Bot Bidding Engine (places bids on behalf of bots in Single Player mode)643 is_single_player = room.get("is_single_player", False)644 bots = [name for name, t in room["teams"].items() if t.get("is_bot", False)]645 646 if is_single_player and bots and room["timer"] > 0:647 # Shuffle bot bidders to ensure fairness648 random.shuffle(bots)649 idx = room["current_player_index"]650 if 0 <= idx < len(room["players"]):651 player = room["players"][idx]652 base_price = player.get("base_price", 1000000)653 rating = player.get("rating", 80)654 655 # Decide price willingness thresholds based on rating656 if rating >= 95:657 max_mult = 8.5658 elif rating >= 90:659 max_mult = 6.0660 elif rating >= 80:661 max_mult = 3.8662 else:663 max_mult = 2.2664 665 next_bid = player["base_price"] if room["current_bid"] == 0 else room["current_bid"] + room["settings"]["min_increment"]666 667 # Check if any bot wants to bid in this second668 for bot_name in bots:669 bot_team = room["teams"][bot_name]670 squad_limit = room["settings"].get("squad_limit", 16)671 remaining_slots = squad_limit - len(bot_team["players"])672 673 # Basic validations674 if remaining_slots <= 0:675 continue676 if next_bid > bot_team["budget"]:677 continue678 if bot_name == room["current_bidder"]:679 continue680 681 # Calculate deterministic valuation modifier based on bot name and player ID682 bot_hash = sum(ord(c) for c in bot_name) + player.get("id", 0)683 modifier = 1.2 + (bot_hash % 100) / 100.0 * (max_mult - 1.2)684 685 # Adjust modifier based on budget abundance:686 # Bots with more budget per remaining slot bid more aggressively!687 starting_budget = room["settings"].get("budget", 1000000000)688 starting_budget_per_slot = starting_budget / squad_limit689 current_budget_per_slot = bot_team["budget"] / remaining_slots690 691 budget_factor = current_budget_per_slot / starting_budget_per_slot692 budget_factor = max(0.6, min(1.8, budget_factor))693 694 max_willing = int(base_price * modifier * budget_factor)695 696 # Limit bots from overcommitting too early on a single player697 budget_cap = bot_team["budget"] * (0.65 if rating >= 90 else 0.40)698 if max_willing > budget_cap:699 max_willing = int(budget_cap)700 701 if next_bid <= max_willing:702 # 35% chance to bid in this second to simulate human hesitation703 if random.random() < 0.35:704 room["current_bid"] = next_bid705 room["current_bidder"] = bot_name706 # Reset timer707 room["timer"] = room["settings"]["timer_duration"]708 709 # Broadcast bid710 bid_payload = {711 "room": get_serializable_room(room),712 "bidder": bot_name,713 "amount": next_bid,714 "timer": room["timer"]715 }716 # Log the bid717 bid_msg = f"โก {bot_name} bid {format_currency_python(next_bid)}"718 room["logs"].append(bid_msg)719 720 save_room_to_disk(room_code)721 broadcast(room_code, "bid_placed", bid_payload)722 break # Only one bot bid per second723 724 if room["timer"] > 0:725 room["timer"] -= 1726 # Broadcast timer tick727 broadcast(room_code, "timer", {728 "timer": room["timer"],729 "timer_active": room["timer_active"]730 })731 if room["timer"] == 0:732 if room["settings"].get("bot_auctioneer"):733 if room["current_bidder"] is None:734 unsold_current_player(room_code)735 else:736 sell_current_player(room_code)737 else:738 room["logs"].append("โฐ Timer reached 0! Awaiting Host action...")739 broadcast(room_code, "state_update", get_serializable_room(room))740 elif (room["status"] in ("sold_pause", "unsold_pause")) and room["settings"].get("bot_auctioneer"):741 if "bot_delay" not in room:742 room["bot_delay"] = 3743 if room["bot_delay"] > 0:744 room["bot_delay"] -= 1745 if room["bot_delay"] == 0:746 role_filter = room.get("current_role_filter", "All")747 advance_to_next_player(room_code, role_filter)748 broadcast(room_code, "state_update", get_serializable_room(room))749 750# Start background timer thread751timer_thread = threading.Thread(target=timer_worker, daemon=True)752timer_thread.start()753 754class AuctionHTTPHandler(SimpleHTTPRequestHandler):755 def end_headers(self):756 # Add standard headers757 self.send_header('Access-Control-Allow-Origin', '*')758 super().end_headers()759 760 def do_OPTIONS(self):761 self.send_response(200)762 self.send_header('Access-Control-Allow-Origin', '*')763 self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')764 self.send_header('Access-Control-Allow-Headers', 'Content-Type')765 self.end_headers()766 767 def do_GET(self):768 url_parsed = urlparse(self.path)769 path = url_parsed.path770 771 if path == '/api/presets':772 self.send_json_response(200, PRESETS)773 return774 775 if path == '/api/config':776 self.send_json_response(200, {})777 return778 779 # Real-time event stream via Server-Sent Events (SSE)780 if path == '/events':781 query = parse_qs(url_parsed.query)782 room_code = query.get('room', [''])[0].upper()783 client_id = query.get('clientId', [''])[0]784 785 if not room_code or room_code not in rooms:786 self.send_response(404)787 self.end_headers()788 self.wfile.write(b"Room not found")789 return790 791 # Prepare streaming headers792 self.send_response(200)793 self.send_header('Content-Type', 'text/event-stream')794 self.send_header('Cache-Control', 'no-cache')795 self.send_header('Connection', 'keep-alive')796 self.send_header('X-Accel-Buffering', 'no')797 self.end_headers()798 799 # Create thread-safe queue for this connection800 q = queue.Queue()801 with rooms_lock:802 room = rooms[room_code]803 # If client_id already exists, replace it to clean up old connections804 room["clients"] = [c for c in room["clients"] if c[0] != client_id]805 room["clients"].append((client_id, q))806 807 # Immediately push initial room status to this client808 init_payload = {809 "type": "init",810 "data_json": json.dumps(get_serializable_room(room)),811 "timestamp": time.time()812 }813 q.put(init_payload)814 815 # Enable aggressive TCP keepalive on this persistent socket to detect client drops quickly816 import socket817 try:818 self.connection.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)819 # Platform-specific TCP keep-alive settings (10 seconds idle, 5 seconds interval, 3 probes)820 if hasattr(socket, 'TCP_KEEPIDLE'):821 self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 10)822 if hasattr(socket, 'TCP_KEEPINTVL'):823 self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 5)824 if hasattr(socket, 'TCP_KEEPCNT'):825 self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3)826 except Exception:827 pass828 829 # Streaming loop830 try:831 while True:832 # Thread Leak Prevention: Check if this connection's queue is still registered.833 # If it has been replaced (e.g. page refresh) or room was deleted, exit the thread.834 with rooms_lock:835 if room_code not in rooms:836 break837 active_queues = [c[1] for c in rooms[room_code]["clients"]]838 if q not in active_queues:839 break840 841 try:842 # Wait for a new event with 5s timeout to send heartbeat keep-alive843 msg = q.get(timeout=5.0)844 event_str = f"event: {msg['type']}\ndata: {msg['data_json']}\n\n"845 self.wfile.write(event_str.encode('utf-8'))846 self.wfile.flush()847 except queue.Empty:848 # Keep connection alive with an SSE comment line849 self.wfile.write(b": keep-alive\n\n")850 self.wfile.flush()851 except (ConnectionResetError, BrokenPipeError, Exception) as e:852 # Connection was closed by client853 pass854 finally:855 # Remove client queue856 with rooms_lock:857 if room_code in rooms:858 rooms[room_code]["clients"] = [c for c in rooms[room_code]["clients"] if c[1] != q]859 return860 861 # Fall back to serving static files from public directory862 super().do_GET()863 864 def do_POST(self):865 url_parsed = urlparse(self.path)866 path = url_parsed.path867 868 # Check if route starts with API prefix869 if path.startswith('/api/'):870 content_length = int(self.headers.get('Content-Length', 0))871 post_data = self.rfile.read(content_length)872 873 try:874 data = json.loads(post_data.decode('utf-8')) if post_data else {}875 except Exception:876 self.send_json_response(400, {"error": "Invalid JSON"})877 return878 879 if path == '/api/create':880 self.handle_create(data)881 elif path == '/api/join':882 self.handle_join(data)883 elif path == '/api/bid':884 self.handle_bid(data)885 elif path == '/api/control':886 self.handle_control(data)887 elif path == '/api/trade':888 self.handle_trade(data)889 elif path == '/api/presets':890 self.send_json_response(200, PRESETS)891 elif path == '/api/auth/register':892 self.handle_register(data)893 elif path == '/api/auth/login':894 self.handle_login(data)895 elif path == '/api/chat':896 self.handle_chat(data)897 elif path == '/api/history':898 self.handle_history(data)899 elif path == '/api/room':900 self.handle_get_room(data)901 elif path == '/api/delete_room':902 self.handle_delete_room(data)903 else:904 self.send_json_response(404, {"error": "Not Found"})905 else:906 self.send_json_response(404, {"error": "Not Found"})907 908 def send_json_response(self, status, payload):909 self.send_response(status)910 self.send_header('Content-Type', 'application/json')911 self.end_headers()912 self.wfile.write(json.dumps(payload).encode('utf-8'))913 914 def handle_register(self, data):915 username = data.get('username', '').strip()916 password = data.get('password', '').strip()917 918 if not username or not password:919 self.send_json_response(400, {"error": "Username and password are required"})920 return921 922 if len(username) < 3 or len(username) > 20:923 self.send_json_response(400, {"error": "Username must be between 3 and 20 characters"})924 return925 926 if not all(c.isalnum() or c in '_-' for c in username):927 self.send_json_response(400, {"error": "Username can only contain alphanumeric characters, dashes, and underscores"})928 return929 930 with users_lock:931 if username in users:932 self.send_json_response(400, {"error": "Username is already taken"})933 return934 935 hashed, salt = hash_password(password)936 users[username] = {937 "password_hash": hashed,938 "salt": salt939 }940 db.save_user(username, hashed, salt)941 942 token = create_session(username)943 self.send_json_response(200, {944 "success": True, 945 "auth_token": token,946 "username": username947 })948 949 def handle_login(self, data):950 username = data.get('username', '').strip()951 password = data.get('password', '').strip()952 953 if not username or not password:954 self.send_json_response(400, {"error": "Username and password are required"})955 return956 957 with users_lock:958 if username not in users:959 self.send_json_response(400, {"error": "Invalid username or password"})960 return961 962 user_data = users[username]963 hashed, _ = hash_password(password, user_data["salt"])964 if hashed != user_data["password_hash"]:965 self.send_json_response(400, {"error": "Invalid username or password"})966 return967 968 token = create_session(username)969 self.send_json_response(200, {970 "success": True,971 "auth_token": token,972 "username": username973 })974 975 def handle_create(self, data):976 auth_token = data.get('auth_token')977 username = get_user_from_token(auth_token)978 if not username:979 self.send_json_response(401, {"error": "Authentication is required to host an auction."})980 return981 982 host_name = data.get('host_name', 'Host')983 auction_name = data.get('auction_name', 'Cricket Auction')984 settings = data.get('settings', {})985 custom_players = data.get('players', [])986 preset_key = data.get('preset', 'ipl_legends')987 988 is_single_player = bool(data.get('is_single_player', False))989 user_team = data.get('user_team')990 bots = data.get('bots', [])991 992 # Resolve players (preset or custom)993 players = []994 if preset_key in PRESETS and not custom_players:995 players = json.loads(json.dumps(PRESETS[preset_key])) # deep copy996 else:997 # Clean up and load custom players998 players = custom_players999 1000 if not players:1001 self.send_json_response(400, {"error": "No players specified"})1002 return1003 1004 # Randomize draft order by shuffling the players pool1005 random.shuffle(players)1006 1007 budget = int(settings.get('budget', 1500000000))1008 min_increment = int(settings.get('min_increment', 500000))1009 timer_duration = int(settings.get('timer_duration', 15))1010 overseas_limit = int(settings.get('overseas_limit', 999))1011 bot_auctioneer = bool(settings.get('bot_auctioneer', False))1012 1013 # Generate clean Room Code1014 with rooms_lock:1015 room_code = ''1016 for _ in range(10): # try 10 times to get unique1017 code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))1018 # replace visually confusing letters/numbers1019 code = code.replace('O', 'K').replace('0', '7').replace('I', 'X').replace('1', '9')1020 if code not in rooms:1021 room_code = code1022 break1023 if not room_code:1024 room_code = "ROOM" + str(random.randint(10, 99))1025 1026 host_id = ''.join(random.choices(string.ascii_lowercase + string.digits, k=12))1027 1028 # Pre-populate teams if single player mode is active1029 teams_dict = {}1030 logs_list = [f"๐ Room {room_code} created by {host_name}."]1031 1032 if is_single_player:1033 if user_team:1034 u_team_name = user_team.get("team", "User Team").strip()1035 u_mgr_name = user_team.get("manager", "Manager Pro").strip()1036 teams_dict[u_team_name] = {1037 "username": username,1038 "manager": u_mgr_name,1039 "budget": budget,1040 "players": [],1041 "slots": {"Batsman": 0, "Bowler": 0, "All-Rounder": 0, "Wicket-Keeper": 0}1042 }1043 logs_list.append(f"๐ {u_team_name} (Manager: {u_mgr_name}) joined the draft.")1044 1045 for bot in bots:1046 b_team_name = bot.get("team", "").strip()1047 b_mgr_name = bot.get("manager", "").strip()1048 if b_team_name and b_mgr_name:1049 teams_dict[b_team_name] = {1050 "username": "bot_user",1051 "manager": b_mgr_name,1052 "budget": budget,1053 "players": [],1054 "slots": {"Batsman": 0, "Bowler": 0, "All-Rounder": 0, "Wicket-Keeper": 0},1055 "is_bot": True1056 }1057 logs_list.append(f"๐ค Bot Franchise {b_team_name} (Manager: {b_mgr_name}) entered the lobby.")1058 else:1059 logs_list.append("๐ Waiting for managers to join...")1060 1061 rooms[room_code] = {1062 "host_username": username,1063 "created_at": time.strftime("%Y-%m-%d %H:%M:%S"),1064 "auction_name": auction_name,1065 "host_name": host_name,1066 "host_id": host_id,1067 "is_single_player": is_single_player,1068 "settings": {1069 "budget": budget,1070 "min_increment": min_increment,1071 "timer_duration": timer_duration,1072 "overseas_limit": overseas_limit,1073 "bot_auctioneer": bot_auctioneer1074 },1075 "players": players,1076 "current_player_index": -1,1077 "current_bid": 0,1078 "current_bidder": None,1079 "timer": timer_duration,1080 "timer_active": False,1081 "teams": teams_dict,1082 "logs": logs_list,1083 "clients": [],1084 "status": "lobby",1085 "current_role_filter": "All",1086 "bot_delay": 0,1087 "season": 1,1088 "standings": {},1089 "trades": [],1090 "retentions": {},1091 "retention_locked": {},1092 "trade_window_open": False,1093 "last_completed_player_index": None,1094 "completed_players_history": []1095 }1096 save_room_to_disk(room_code)1097 1098 self.send_json_response(200, {1099 "room_code": room_code,1100 "host_id": host_id,1101 "room_state": get_serializable_room(rooms[room_code])1102 })1103 1104 def handle_join(self, data):1105 room_code = data.get('room_code', '').upper()1106 team_name = data.get('team_name', '').strip()1107 manager_name = data.get('manager_name', '').strip()1108 auth_token = data.get('auth_token')1109 1110 if not room_code or room_code not in rooms:1111 self.send_json_response(404, {"error": "Room not found"})1112 return1113 1114 if not team_name or not manager_name:1115 self.send_json_response(400, {"error": "Team and Manager name are required"})1116 return1117 1118 username = get_user_from_token(auth_token)1119 if not username:1120 self.send_json_response(401, {"error": "Authentication is required to join rooms."})1121 return1122 1123 with rooms_lock:1124 room = rooms[room_code]1125 1126 # Enforce unique Username per room code (prevent one user from creating multiple franchises)1127 for existing_team_name, existing_team in room["teams"].items():1128 if existing_team.get("username") == username:1129 if existing_team_name != team_name:1130 self.send_json_response(400, {1131 "error": f"You have already joined this room as team '{existing_team_name}'."1132 })1133 return1134 1135 # Allow reconnecting if details and Username match1136 if team_name in room["teams"]:1137 if room["teams"][team_name].get("username") != username:1138 self.send_json_response(400, {"error": "Team Name is already taken!"})1139 return1140 else:1141 # Limit room to 12 users1142 if len(room["teams"]) >= 12:1143 self.send_json_response(400, {"error": "Room is full! Maximum 12 teams allowed."})1144 return1145 # Setup new team structure1146 room["teams"][team_name] = {1147 "username": username,1148 "manager": manager_name,1149 "budget": room["settings"]["budget"],1150 "players": [],1151 "slots": {"Batsman": 0, "Bowler": 0, "All-Rounder": 0, "Wicket-Keeper": 0}1152 }1153 join_msg = f"๐ {team_name} (Manager: {manager_name}) entered the lobby!"1154 room["logs"].append(join_msg)1155 1156 save_room_to_disk(room_code)1157 broadcast(room_code, "state_update", get_serializable_room(room))1158 1159 self.send_json_response(200, {1160 "room_code": room_code,1161 "team_name": team_name,1162 "manager_name": manager_name,1163 "room_state": get_serializable_room(room)1164 })1165 1166 def handle_bid(self, data):1167 room_code = data.get('room_code', '').upper()1168 team_name = data.get('team_name', '')1169 amount = int(data.get('amount', 0))1170 1171 if room_code not in rooms:1172 self.send_json_response(404, {"error": "Room not found"})1173 return1174 1175 with rooms_lock:1176 room = rooms[room_code]1177 1178 # Validations1179 if room["status"] != "active" or not room["timer_active"]:1180 self.send_json_response(400, {"error": "Bidding is currently closed."})1181 return1182 1183 idx = room["current_player_index"]1184 if idx < 0 or idx >= len(room["players"]):1185 self.send_json_response(400, {"error": "No active player for bidding."})1186 return1187 1188 player = room["players"][idx]1189 1190 if team_name not in room["teams"]:1191 self.send_json_response(403, {"error": "Team not registered in this room."})1192 return1193 1194 team = room["teams"][team_name]1195 1196 # Check maximum squad size1197 squad_limit = room["settings"].get("squad_limit", 16)1198 if len(team["players"]) >= squad_limit:1199 self.send_json_response(400, {"error": f"Squad is full! Maximum {squad_limit} players allowed per team."})1200 return