smodusermc commited on
Commit
ba0aede
·
verified ·
1 Parent(s): e9c054c

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +568 -0
app.py ADDED
@@ -0,0 +1,568 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import uuid
4
+ import time
5
+ from datetime import datetime, timedelta
6
+ from flask import Flask, render_template, request, send_from_directory
7
+ from flask_socketio import SocketIO, emit, join_room, leave_room
8
+ from flask_sqlalchemy import SQLAlchemy
9
+ import threading
10
+
11
+ # Initialize Flask app with explicit static folder
12
+ app = Flask(__name__,
13
+ static_folder='static',
14
+ static_url_path='/static',
15
+ template_folder='templates')
16
+
17
+ app.config['SECRET_KEY'] = os.urandom(24).hex()
18
+ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///chess_games.db'
19
+ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
20
+ app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0 # Disable caching for development
21
+
22
+ db = SQLAlchemy(app)
23
+ socketio = SocketIO(app, cors_allowed_origins="*", async_mode='eventlet',
24
+ ping_timeout=60, ping_interval=25)
25
+
26
+ # ========================================
27
+ # Database Models
28
+ # ========================================
29
+ class SavedGame(db.Model):
30
+ id = db.Column(db.String(36), primary_key=True)
31
+ room_id = db.Column(db.String(36), nullable=False)
32
+ fen = db.Column(db.String(100), nullable=False)
33
+ pgn = db.Column(db.Text, nullable=True)
34
+ white_player = db.Column(db.String(50), nullable=False)
35
+ black_player = db.Column(db.String(50), nullable=False)
36
+ white_time = db.Column(db.Integer, nullable=True)
37
+ black_time = db.Column(db.Integer, nullable=True)
38
+ time_control = db.Column(db.Integer, nullable=True)
39
+ current_turn = db.Column(db.String(1), nullable=False, default='w')
40
+ move_history = db.Column(db.Text, nullable=True)
41
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
42
+ expires_at = db.Column(db.DateTime, nullable=False)
43
+ status = db.Column(db.String(20), default='paused')
44
+
45
+ class GameHistory(db.Model):
46
+ id = db.Column(db.String(36), primary_key=True)
47
+ white_player = db.Column(db.String(50), nullable=False)
48
+ black_player = db.Column(db.String(50), nullable=False)
49
+ winner = db.Column(db.String(50), nullable=True)
50
+ result = db.Column(db.String(20), nullable=False)
51
+ pgn = db.Column(db.Text, nullable=True)
52
+ time_control = db.Column(db.Integer, nullable=True)
53
+ played_at = db.Column(db.DateTime, default=datetime.utcnow)
54
+
55
+ # ========================================
56
+ # In-memory Storage
57
+ # ========================================
58
+ rooms = {}
59
+ players = {}
60
+ disconnected_players = {}
61
+
62
+ def generate_room_id():
63
+ return str(uuid.uuid4())[:8].upper()
64
+
65
+ def cleanup_expired_games():
66
+ """Remove expired saved games"""
67
+ while True:
68
+ try:
69
+ with app.app_context():
70
+ expired = SavedGame.query.filter(SavedGame.expires_at < datetime.utcnow()).all()
71
+ for game in expired:
72
+ db.session.delete(game)
73
+ db.session.commit()
74
+ except Exception as e:
75
+ print(f"Cleanup error: {e}")
76
+ time.sleep(60)
77
+
78
+ # ========================================
79
+ # Routes
80
+ # ========================================
81
+ @app.route('/')
82
+ def index():
83
+ return render_template('index.html')
84
+
85
+ @app.route('/static/<path:filename>')
86
+ def serve_static(filename):
87
+ return send_from_directory(app.static_folder, filename)
88
+
89
+ @app.route('/health')
90
+ def health():
91
+ return {'status': 'healthy'}
92
+
93
+ # ========================================
94
+ # Socket Events
95
+ # ========================================
96
+ @socketio.on('connect')
97
+ def handle_connect():
98
+ print(f"Client connected: {request.sid}")
99
+ emit('connected', {'sid': request.sid})
100
+
101
+ @socketio.on('disconnect')
102
+ def handle_disconnect():
103
+ sid = request.sid
104
+ print(f"Client disconnected: {sid}")
105
+
106
+ if sid in players:
107
+ player_info = players[sid]
108
+ room_id = player_info.get('room')
109
+ username = player_info.get('username')
110
+
111
+ if room_id and room_id in rooms:
112
+ room = rooms[room_id]
113
+
114
+ # Mark player as disconnected
115
+ disconnected_players[sid] = {
116
+ 'username': username,
117
+ 'room': room_id,
118
+ 'color': player_info.get('color'),
119
+ 'disconnected_at': datetime.utcnow()
120
+ }
121
+
122
+ # Save game state
123
+ save_game_state(room_id)
124
+
125
+ # Notify opponent
126
+ emit('opponent_disconnected', {
127
+ 'username': username,
128
+ 'timeout': 600
129
+ }, room=room_id)
130
+
131
+ # Start reconnection timer
132
+ threading.Thread(target=check_reconnection, args=(sid, room_id, 600), daemon=True).start()
133
+
134
+ del players[sid]
135
+
136
+ def check_reconnection(sid, room_id, timeout):
137
+ """Check if player reconnects within timeout"""
138
+ time.sleep(timeout)
139
+
140
+ if sid in disconnected_players:
141
+ with app.app_context():
142
+ if room_id in rooms:
143
+ room = rooms[room_id]
144
+ disconnected = disconnected_players[sid]
145
+ winner_color = 'black' if disconnected['color'] == 'white' else 'white'
146
+
147
+ winner = None
148
+ for p in room.get('players', []):
149
+ if p.get('color') == winner_color:
150
+ winner = p.get('username')
151
+ break
152
+
153
+ socketio.emit('game_over', {
154
+ 'reason': 'abandonment',
155
+ 'winner': winner,
156
+ 'message': f"{disconnected['username']} abandoned the game"
157
+ }, room=room_id)
158
+
159
+ save_game_history(room_id, winner, 'abandonment')
160
+ cleanup_room(room_id)
161
+
162
+ del disconnected_players[sid]
163
+
164
+ def save_game_state(room_id):
165
+ """Save current game state to database"""
166
+ if room_id not in rooms:
167
+ return
168
+
169
+ room = rooms[room_id]
170
+ game_state = room.get('game_state', {})
171
+
172
+ try:
173
+ saved_game = SavedGame(
174
+ id=str(uuid.uuid4()),
175
+ room_id=room_id,
176
+ fen=game_state.get('fen', 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'),
177
+ pgn=game_state.get('pgn', ''),
178
+ white_player=room.get('white', ''),
179
+ black_player=room.get('black', ''),
180
+ white_time=room.get('white_time'),
181
+ black_time=room.get('black_time'),
182
+ time_control=room.get('time_control'),
183
+ current_turn=game_state.get('turn', 'w'),
184
+ move_history=json.dumps(game_state.get('moves', [])),
185
+ expires_at=datetime.utcnow() + timedelta(minutes=10),
186
+ status='paused'
187
+ )
188
+ db.session.add(saved_game)
189
+ db.session.commit()
190
+ except Exception as e:
191
+ print(f"Error saving game: {e}")
192
+ db.session.rollback()
193
+
194
+ def save_game_history(room_id, winner, result):
195
+ """Save completed game to history"""
196
+ if room_id not in rooms:
197
+ return
198
+
199
+ room = rooms[room_id]
200
+
201
+ try:
202
+ history = GameHistory(
203
+ id=str(uuid.uuid4()),
204
+ white_player=room.get('white', ''),
205
+ black_player=room.get('black', ''),
206
+ winner=winner,
207
+ result=result,
208
+ pgn=room.get('game_state', {}).get('pgn', ''),
209
+ time_control=room.get('time_control')
210
+ )
211
+ db.session.add(history)
212
+ db.session.commit()
213
+ except Exception as e:
214
+ print(f"Error saving history: {e}")
215
+ db.session.rollback()
216
+
217
+ def cleanup_room(room_id):
218
+ """Clean up room data"""
219
+ if room_id in rooms:
220
+ del rooms[room_id]
221
+
222
+ @socketio.on('create_room')
223
+ def handle_create_room(data):
224
+ username = data.get('username', 'Anonymous')
225
+ time_control = data.get('time_control')
226
+
227
+ room_id = generate_room_id()
228
+
229
+ rooms[room_id] = {
230
+ 'players': [],
231
+ 'game_state': {
232
+ 'fen': 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1',
233
+ 'turn': 'w',
234
+ 'moves': [],
235
+ 'pgn': ''
236
+ },
237
+ 'time_control': time_control,
238
+ 'white_time': time_control * 60 if time_control else None,
239
+ 'black_time': time_control * 60 if time_control else None,
240
+ 'started': False,
241
+ 'white': None,
242
+ 'black': None,
243
+ 'last_move_time': None
244
+ }
245
+
246
+ join_room(room_id)
247
+
248
+ color = 'white'
249
+ rooms[room_id]['players'].append({
250
+ 'sid': request.sid,
251
+ 'username': username,
252
+ 'color': color
253
+ })
254
+ rooms[room_id]['white'] = username
255
+
256
+ players[request.sid] = {
257
+ 'username': username,
258
+ 'room': room_id,
259
+ 'color': color
260
+ }
261
+
262
+ emit('room_created', {
263
+ 'room_id': room_id,
264
+ 'color': color,
265
+ 'time_control': time_control
266
+ })
267
+
268
+ @socketio.on('join_room')
269
+ def handle_join_room(data):
270
+ username = data.get('username', 'Anonymous')
271
+ room_id = data.get('room_id', '').upper().strip()
272
+
273
+ if room_id not in rooms:
274
+ emit('error', {'message': 'Room not found'})
275
+ return
276
+
277
+ room = rooms[room_id]
278
+
279
+ if len(room['players']) >= 2:
280
+ emit('error', {'message': 'Room is full'})
281
+ return
282
+
283
+ join_room(room_id)
284
+
285
+ color = 'black'
286
+ room['players'].append({
287
+ 'sid': request.sid,
288
+ 'username': username,
289
+ 'color': color
290
+ })
291
+ room['black'] = username
292
+
293
+ players[request.sid] = {
294
+ 'username': username,
295
+ 'room': room_id,
296
+ 'color': color
297
+ }
298
+
299
+ # Check if this is a reconnection
300
+ saved_game = None
301
+ try:
302
+ saved_game = SavedGame.query.filter_by(room_id=room_id, status='paused').first()
303
+ except:
304
+ pass
305
+
306
+ if saved_game:
307
+ room['game_state'] = {
308
+ 'fen': saved_game.fen,
309
+ 'turn': saved_game.current_turn,
310
+ 'moves': json.loads(saved_game.move_history) if saved_game.move_history else [],
311
+ 'pgn': saved_game.pgn or ''
312
+ }
313
+ room['white_time'] = saved_game.white_time
314
+ room['black_time'] = saved_game.black_time
315
+
316
+ db.session.delete(saved_game)
317
+ db.session.commit()
318
+
319
+ emit('game_restored', {
320
+ 'room_id': room_id,
321
+ 'color': color,
322
+ 'game_state': room['game_state'],
323
+ 'white_time': room['white_time'],
324
+ 'black_time': room['black_time'],
325
+ 'opponent': room['white']
326
+ })
327
+
328
+ emit('opponent_reconnected', {
329
+ 'username': username,
330
+ 'game_state': room['game_state'],
331
+ 'white_time': room['white_time'],
332
+ 'black_time': room['black_time']
333
+ }, room=room_id, include_self=False)
334
+ else:
335
+ emit('room_joined', {
336
+ 'room_id': room_id,
337
+ 'color': color,
338
+ 'time_control': room['time_control'],
339
+ 'opponent': room['white']
340
+ })
341
+
342
+ emit('opponent_joined', {
343
+ 'username': username,
344
+ 'color': 'black'
345
+ }, room=room_id, include_self=False)
346
+
347
+ if len(room['players']) == 2:
348
+ room['started'] = True
349
+ room['last_move_time'] = time.time()
350
+ emit('game_start', {
351
+ 'white': room['white'],
352
+ 'black': room['black'],
353
+ 'time_control': room['time_control'],
354
+ 'game_state': room['game_state']
355
+ }, room=room_id)
356
+
357
+ @socketio.on('make_move')
358
+ def handle_move(data):
359
+ sid = request.sid
360
+
361
+ if sid not in players:
362
+ emit('error', {'message': 'Player not found'})
363
+ return
364
+
365
+ player = players[sid]
366
+ room_id = player['room']
367
+
368
+ if room_id not in rooms:
369
+ emit('error', {'message': 'Room not found'})
370
+ return
371
+
372
+ room = rooms[room_id]
373
+
374
+ if not room['started']:
375
+ emit('error', {'message': 'Game not started'})
376
+ return
377
+
378
+ move = data.get('move')
379
+ fen = data.get('fen')
380
+ pgn = data.get('pgn', '')
381
+ turn = data.get('turn')
382
+
383
+ # Update time
384
+ if room['time_control']:
385
+ current_time = time.time()
386
+ if room['last_move_time']:
387
+ elapsed = current_time - room['last_move_time']
388
+
389
+ if room['game_state']['turn'] == 'w':
390
+ room['white_time'] = max(0, room['white_time'] - elapsed)
391
+ if room['white_time'] <= 0:
392
+ emit('game_over', {
393
+ 'reason': 'timeout',
394
+ 'winner': room['black'],
395
+ 'loser_color': 'white'
396
+ }, room=room_id)
397
+ save_game_history(room_id, room['black'], 'timeout')
398
+ cleanup_room(room_id)
399
+ return
400
+ else:
401
+ room['black_time'] = max(0, room['black_time'] - elapsed)
402
+ if room['black_time'] <= 0:
403
+ emit('game_over', {
404
+ 'reason': 'timeout',
405
+ 'winner': room['white'],
406
+ 'loser_color': 'black'
407
+ }, room=room_id)
408
+ save_game_history(room_id, room['white'], 'timeout')
409
+ cleanup_room(room_id)
410
+ return
411
+
412
+ room['last_move_time'] = current_time
413
+
414
+ # Update game state
415
+ room['game_state']['fen'] = fen
416
+ room['game_state']['turn'] = turn
417
+ room['game_state']['moves'].append(move)
418
+ room['game_state']['pgn'] = pgn
419
+
420
+ # Broadcast move
421
+ emit('move_made', {
422
+ 'move': move,
423
+ 'fen': fen,
424
+ 'pgn': pgn,
425
+ 'turn': turn,
426
+ 'white_time': room['white_time'],
427
+ 'black_time': room['black_time'],
428
+ 'is_check': data.get('is_check', False),
429
+ 'is_checkmate': data.get('is_checkmate', False),
430
+ 'is_draw': data.get('is_draw', False),
431
+ 'is_stalemate': data.get('is_stalemate', False)
432
+ }, room=room_id)
433
+
434
+ # Check for game end
435
+ if data.get('is_checkmate'):
436
+ winner = room['black'] if turn == 'w' else room['white']
437
+ save_game_history(room_id, winner, 'checkmate')
438
+ elif data.get('is_draw') or data.get('is_stalemate'):
439
+ save_game_history(room_id, None, 'draw')
440
+
441
+ @socketio.on('resign')
442
+ def handle_resign():
443
+ sid = request.sid
444
+
445
+ if sid not in players:
446
+ return
447
+
448
+ player = players[sid]
449
+ room_id = player['room']
450
+
451
+ if room_id not in rooms:
452
+ return
453
+
454
+ room = rooms[room_id]
455
+ winner = room['black'] if player['color'] == 'white' else room['white']
456
+
457
+ emit('game_over', {
458
+ 'reason': 'resignation',
459
+ 'winner': winner,
460
+ 'resigned': player['username']
461
+ }, room=room_id)
462
+
463
+ save_game_history(room_id, winner, 'resignation')
464
+ cleanup_room(room_id)
465
+
466
+ @socketio.on('offer_draw')
467
+ def handle_draw_offer():
468
+ sid = request.sid
469
+
470
+ if sid not in players:
471
+ return
472
+
473
+ player = players[sid]
474
+ room_id = player['room']
475
+
476
+ emit('draw_offered', {
477
+ 'from': player['username']
478
+ }, room=room_id, include_self=False)
479
+
480
+ @socketio.on('accept_draw')
481
+ def handle_draw_accept():
482
+ sid = request.sid
483
+
484
+ if sid not in players:
485
+ return
486
+
487
+ player = players[sid]
488
+ room_id = player['room']
489
+
490
+ emit('game_over', {
491
+ 'reason': 'draw',
492
+ 'winner': None
493
+ }, room=room_id)
494
+
495
+ save_game_history(room_id, None, 'draw_agreement')
496
+ cleanup_room(room_id)
497
+
498
+ @socketio.on('decline_draw')
499
+ def handle_draw_decline():
500
+ sid = request.sid
501
+
502
+ if sid not in players:
503
+ return
504
+
505
+ player = players[sid]
506
+ room_id = player['room']
507
+
508
+ emit('draw_declined', {
509
+ 'by': player['username']
510
+ }, room=room_id, include_self=False)
511
+
512
+ @socketio.on('chat_message')
513
+ def handle_chat(data):
514
+ sid = request.sid
515
+
516
+ if sid not in players:
517
+ return
518
+
519
+ player = players[sid]
520
+ room_id = player['room']
521
+
522
+ emit('chat_message', {
523
+ 'username': player['username'],
524
+ 'message': data.get('message', '')[:500],
525
+ 'timestamp': datetime.utcnow().isoformat()
526
+ }, room=room_id)
527
+
528
+ @socketio.on('get_rooms')
529
+ def handle_get_rooms():
530
+ available_rooms = []
531
+ for room_id, room in rooms.items():
532
+ if len(room['players']) < 2:
533
+ available_rooms.append({
534
+ 'room_id': room_id,
535
+ 'host': room['white'],
536
+ 'time_control': room['time_control']
537
+ })
538
+ emit('rooms_list', {'rooms': available_rooms})
539
+
540
+ @socketio.on('leave_room')
541
+ def handle_leave_room():
542
+ sid = request.sid
543
+ if sid in players:
544
+ player = players[sid]
545
+ room_id = player.get('room')
546
+ if room_id:
547
+ leave_room(room_id)
548
+ if room_id in rooms:
549
+ room = rooms[room_id]
550
+ room['players'] = [p for p in room['players'] if p['sid'] != sid]
551
+ if len(room['players']) == 0:
552
+ del rooms[room_id]
553
+ del players[sid]
554
+
555
+ # ========================================
556
+ # Main
557
+ # ========================================
558
+ if __name__ == '__main__':
559
+ with app.app_context():
560
+ db.create_all()
561
+
562
+ # Start cleanup thread
563
+ cleanup_thread = threading.Thread(target=cleanup_expired_games, daemon=True)
564
+ cleanup_thread.start()
565
+
566
+ print("Starting Chess Arena on http://0.0.0.0:7860")
567
+ socketio.run(app, host='0.0.0.0', port=7860, debug=False, allow_unsafe_werkzeug=True)
568
+