CoolFace
Apppublic

nextgen45/cricket-auction

sourceHugging Faceupdated 2mo agoView on Hugging Face
1likes
server.py2039 linesDownload Raw Back to root
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

Showing the first 1,200 of 2039 lines. Download the file for the rest.