Spaces:
Paused
Paused
Download app.py from smodusermc/chess: direct link, hf CLI and curl.
- Browser
- Download file 17.3 kB
-
https://huggingface.co/spaces/smodusermc/chess/resolve/main/app.py
- Command line
-
hf download hf://spaces/smodusermc/chess/app.py
-
curl -L -o app.py https://huggingface.co/spaces/smodusermc/chess/resolve/main/app.py
17.3 kB
| 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 | |
| # ======================================== | |
| def index(): | |
| return render_template('index.html') | |
| def serve_static(filename): | |
| return send_from_directory(app.static_folder, filename) | |
| def health(): | |
| return {'status': 'healthy'} | |
| # ======================================== | |
| # Socket Events | |
| # ======================================== | |
| def handle_connect(): | |
| print(f"Client connected: {request.sid}") | |
| emit('connected', {'sid': request.sid}) | |
| 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] | |
| 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 | |
| }) | |
| 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) | |
| 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') | |
| 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) | |
| 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) | |
| 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) | |
| 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) | |
| 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) | |
| 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}) | |
| 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) | |