import os import json import uuid import time from datetime import datetime, timedelta from flask import Flask, render_template, request, send_from_directory from flask_socketio import SocketIO, emit, join_room, leave_room from flask_sqlalchemy import SQLAlchemy import threading # Initialize Flask app with explicit static folder app = Flask(__name__, static_folder='static', static_url_path='/static', template_folder='templates') app.config['SECRET_KEY'] = os.urandom(24).hex() app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///chess_games.db' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0 # Disable caching for development db = SQLAlchemy(app) socketio = SocketIO(app, cors_allowed_origins="*", async_mode='eventlet', ping_timeout=60, ping_interval=25) # ======================================== # Database Models # ======================================== class SavedGame(db.Model): id = db.Column(db.String(36), primary_key=True) room_id = db.Column(db.String(36), nullable=False) fen = db.Column(db.String(100), nullable=False) pgn = db.Column(db.Text, nullable=True) white_player = db.Column(db.String(50), nullable=False) black_player = db.Column(db.String(50), nullable=False) white_time = db.Column(db.Integer, nullable=True) black_time = db.Column(db.Integer, nullable=True) time_control = db.Column(db.Integer, nullable=True) current_turn = db.Column(db.String(1), nullable=False, default='w') move_history = db.Column(db.Text, nullable=True) created_at = db.Column(db.DateTime, default=datetime.utcnow) expires_at = db.Column(db.DateTime, nullable=False) status = db.Column(db.String(20), default='paused') class GameHistory(db.Model): id = db.Column(db.String(36), primary_key=True) white_player = db.Column(db.String(50), nullable=False) black_player = db.Column(db.String(50), nullable=False) winner = db.Column(db.String(50), nullable=True) result = db.Column(db.String(20), nullable=False) pgn = db.Column(db.Text, nullable=True) time_control = db.Column(db.Integer, nullable=True) played_at = db.Column(db.DateTime, default=datetime.utcnow) # ======================================== # In-memory Storage # ======================================== rooms = {} players = {} disconnected_players = {} def generate_room_id(): return str(uuid.uuid4())[:8].upper() def cleanup_expired_games(): """Remove expired saved games""" while True: try: with app.app_context(): expired = SavedGame.query.filter(SavedGame.expires_at < datetime.utcnow()).all() for game in expired: db.session.delete(game) db.session.commit() except Exception as e: print(f"Cleanup error: {e}") time.sleep(60) # ======================================== # Routes # ======================================== @app.route('/') def index(): return render_template('index.html') @app.route('/static/') def serve_static(filename): return send_from_directory(app.static_folder, filename) @app.route('/health') def health(): return {'status': 'healthy'} # ======================================== # Socket Events # ======================================== @socketio.on('connect') def handle_connect(): print(f"Client connected: {request.sid}") emit('connected', {'sid': request.sid}) @socketio.on('disconnect') def handle_disconnect(): sid = request.sid print(f"Client disconnected: {sid}") if sid in players: player_info = players[sid] room_id = player_info.get('room') username = player_info.get('username') if room_id and room_id in rooms: room = rooms[room_id] # Mark player as disconnected disconnected_players[sid] = { 'username': username, 'room': room_id, 'color': player_info.get('color'), 'disconnected_at': datetime.utcnow() } # Save game state save_game_state(room_id) # Notify opponent emit('opponent_disconnected', { 'username': username, 'timeout': 600 }, room=room_id) # Start reconnection timer threading.Thread(target=check_reconnection, args=(sid, room_id, 600), daemon=True).start() del players[sid] def check_reconnection(sid, room_id, timeout): """Check if player reconnects within timeout""" time.sleep(timeout) if sid in disconnected_players: with app.app_context(): if room_id in rooms: room = rooms[room_id] disconnected = disconnected_players[sid] winner_color = 'black' if disconnected['color'] == 'white' else 'white' winner = None for p in room.get('players', []): if p.get('color') == winner_color: winner = p.get('username') break socketio.emit('game_over', { 'reason': 'abandonment', 'winner': winner, 'message': f"{disconnected['username']} abandoned the game" }, room=room_id) save_game_history(room_id, winner, 'abandonment') cleanup_room(room_id) del disconnected_players[sid] def save_game_state(room_id): """Save current game state to database""" if room_id not in rooms: return room = rooms[room_id] game_state = room.get('game_state', {}) try: saved_game = SavedGame( id=str(uuid.uuid4()), room_id=room_id, fen=game_state.get('fen', 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'), pgn=game_state.get('pgn', ''), white_player=room.get('white', ''), black_player=room.get('black', ''), white_time=room.get('white_time'), black_time=room.get('black_time'), time_control=room.get('time_control'), current_turn=game_state.get('turn', 'w'), move_history=json.dumps(game_state.get('moves', [])), expires_at=datetime.utcnow() + timedelta(minutes=10), status='paused' ) db.session.add(saved_game) db.session.commit() except Exception as e: print(f"Error saving game: {e}") db.session.rollback() def save_game_history(room_id, winner, result): """Save completed game to history""" if room_id not in rooms: return room = rooms[room_id] try: history = GameHistory( id=str(uuid.uuid4()), white_player=room.get('white', ''), black_player=room.get('black', ''), winner=winner, result=result, pgn=room.get('game_state', {}).get('pgn', ''), time_control=room.get('time_control') ) db.session.add(history) db.session.commit() except Exception as e: print(f"Error saving history: {e}") db.session.rollback() def cleanup_room(room_id): """Clean up room data""" if room_id in rooms: del rooms[room_id] @socketio.on('create_room') def handle_create_room(data): username = data.get('username', 'Anonymous') time_control = data.get('time_control') room_id = generate_room_id() rooms[room_id] = { 'players': [], 'game_state': { 'fen': 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1', 'turn': 'w', 'moves': [], 'pgn': '' }, 'time_control': time_control, 'white_time': time_control * 60 if time_control else None, 'black_time': time_control * 60 if time_control else None, 'started': False, 'white': None, 'black': None, 'last_move_time': None } join_room(room_id) color = 'white' rooms[room_id]['players'].append({ 'sid': request.sid, 'username': username, 'color': color }) rooms[room_id]['white'] = username players[request.sid] = { 'username': username, 'room': room_id, 'color': color } emit('room_created', { 'room_id': room_id, 'color': color, 'time_control': time_control }) @socketio.on('join_room') def handle_join_room(data): username = data.get('username', 'Anonymous') room_id = data.get('room_id', '').upper().strip() if room_id not in rooms: emit('error', {'message': 'Room not found'}) return room = rooms[room_id] if len(room['players']) >= 2: emit('error', {'message': 'Room is full'}) return join_room(room_id) color = 'black' room['players'].append({ 'sid': request.sid, 'username': username, 'color': color }) room['black'] = username players[request.sid] = { 'username': username, 'room': room_id, 'color': color } # Check if this is a reconnection saved_game = None try: saved_game = SavedGame.query.filter_by(room_id=room_id, status='paused').first() except: pass if saved_game: room['game_state'] = { 'fen': saved_game.fen, 'turn': saved_game.current_turn, 'moves': json.loads(saved_game.move_history) if saved_game.move_history else [], 'pgn': saved_game.pgn or '' } room['white_time'] = saved_game.white_time room['black_time'] = saved_game.black_time db.session.delete(saved_game) db.session.commit() emit('game_restored', { 'room_id': room_id, 'color': color, 'game_state': room['game_state'], 'white_time': room['white_time'], 'black_time': room['black_time'], 'opponent': room['white'] }) emit('opponent_reconnected', { 'username': username, 'game_state': room['game_state'], 'white_time': room['white_time'], 'black_time': room['black_time'] }, room=room_id, include_self=False) else: emit('room_joined', { 'room_id': room_id, 'color': color, 'time_control': room['time_control'], 'opponent': room['white'] }) emit('opponent_joined', { 'username': username, 'color': 'black' }, room=room_id, include_self=False) if len(room['players']) == 2: room['started'] = True room['last_move_time'] = time.time() emit('game_start', { 'white': room['white'], 'black': room['black'], 'time_control': room['time_control'], 'game_state': room['game_state'] }, room=room_id) @socketio.on('make_move') def handle_move(data): sid = request.sid if sid not in players: emit('error', {'message': 'Player not found'}) return player = players[sid] room_id = player['room'] if room_id not in rooms: emit('error', {'message': 'Room not found'}) return room = rooms[room_id] if not room['started']: emit('error', {'message': 'Game not started'}) return move = data.get('move') fen = data.get('fen') pgn = data.get('pgn', '') turn = data.get('turn') # Update time if room['time_control']: current_time = time.time() if room['last_move_time']: elapsed = current_time - room['last_move_time'] if room['game_state']['turn'] == 'w': room['white_time'] = max(0, room['white_time'] - elapsed) if room['white_time'] <= 0: emit('game_over', { 'reason': 'timeout', 'winner': room['black'], 'loser_color': 'white' }, room=room_id) save_game_history(room_id, room['black'], 'timeout') cleanup_room(room_id) return else: room['black_time'] = max(0, room['black_time'] - elapsed) if room['black_time'] <= 0: emit('game_over', { 'reason': 'timeout', 'winner': room['white'], 'loser_color': 'black' }, room=room_id) save_game_history(room_id, room['white'], 'timeout') cleanup_room(room_id) return room['last_move_time'] = current_time # Update game state room['game_state']['fen'] = fen room['game_state']['turn'] = turn room['game_state']['moves'].append(move) room['game_state']['pgn'] = pgn # Broadcast move emit('move_made', { 'move': move, 'fen': fen, 'pgn': pgn, 'turn': turn, 'white_time': room['white_time'], 'black_time': room['black_time'], 'is_check': data.get('is_check', False), 'is_checkmate': data.get('is_checkmate', False), 'is_draw': data.get('is_draw', False), 'is_stalemate': data.get('is_stalemate', False) }, room=room_id) # Check for game end if data.get('is_checkmate'): winner = room['black'] if turn == 'w' else room['white'] save_game_history(room_id, winner, 'checkmate') elif data.get('is_draw') or data.get('is_stalemate'): save_game_history(room_id, None, 'draw') @socketio.on('resign') def handle_resign(): sid = request.sid if sid not in players: return player = players[sid] room_id = player['room'] if room_id not in rooms: return room = rooms[room_id] winner = room['black'] if player['color'] == 'white' else room['white'] emit('game_over', { 'reason': 'resignation', 'winner': winner, 'resigned': player['username'] }, room=room_id) save_game_history(room_id, winner, 'resignation') cleanup_room(room_id) @socketio.on('offer_draw') def handle_draw_offer(): sid = request.sid if sid not in players: return player = players[sid] room_id = player['room'] emit('draw_offered', { 'from': player['username'] }, room=room_id, include_self=False) @socketio.on('accept_draw') def handle_draw_accept(): sid = request.sid if sid not in players: return player = players[sid] room_id = player['room'] emit('game_over', { 'reason': 'draw', 'winner': None }, room=room_id) save_game_history(room_id, None, 'draw_agreement') cleanup_room(room_id) @socketio.on('decline_draw') def handle_draw_decline(): sid = request.sid if sid not in players: return player = players[sid] room_id = player['room'] emit('draw_declined', { 'by': player['username'] }, room=room_id, include_self=False) @socketio.on('chat_message') def handle_chat(data): sid = request.sid if sid not in players: return player = players[sid] room_id = player['room'] emit('chat_message', { 'username': player['username'], 'message': data.get('message', '')[:500], 'timestamp': datetime.utcnow().isoformat() }, room=room_id) @socketio.on('get_rooms') def handle_get_rooms(): available_rooms = [] for room_id, room in rooms.items(): if len(room['players']) < 2: available_rooms.append({ 'room_id': room_id, 'host': room['white'], 'time_control': room['time_control'] }) emit('rooms_list', {'rooms': available_rooms}) @socketio.on('leave_room') def handle_leave_room(): sid = request.sid if sid in players: player = players[sid] room_id = player.get('room') if room_id: leave_room(room_id) if room_id in rooms: room = rooms[room_id] room['players'] = [p for p in room['players'] if p['sid'] != sid] if len(room['players']) == 0: del rooms[room_id] del players[sid] # ======================================== # Main # ======================================== if __name__ == '__main__': with app.app_context(): db.create_all() # Start cleanup thread cleanup_thread = threading.Thread(target=cleanup_expired_games, daemon=True) cleanup_thread.start() print("Starting Chess Arena on http://0.0.0.0:7860") socketio.run(app, host='0.0.0.0', port=7860, debug=False, allow_unsafe_werkzeug=True)