-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver_socketio.py
More file actions
729 lines (602 loc) · 27 KB
/
server_socketio.py
File metadata and controls
729 lines (602 loc) · 27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
from flask import Flask, request, jsonify
from flask_cors import CORS
from flask_socketio import SocketIO, emit, join_room, leave_room
import secrets
from datetime import datetime
from games import games
from games.image_autogui_data import *
from games.models import Value, Remoteness
from md_api import md_instr
from threading import Timer
import time
app = Flask(__name__)
CORS(app, resources={r"/*": {"origins": "*"}})
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading')
# Store active game rooms
# Structure: {room_id: {players: [sid1, sid2], spectators: [sid3, ...], game_data: {...}, created_at: timestamp, is_public: bool}}
game_rooms = {}
# Store player/spectator connections
# Structure: {sid: {room_id: str, role: 'player' | 'spectator', player_index: int}}
connections = {}
def cleanup_stale_rooms():
"""Remove rooms that have been empty or inactive for too long"""
current_time = datetime.now()
rooms_to_remove = []
for room_id, room_data in list(game_rooms.items()): # Use list() to avoid RuntimeError
# Check if room is empty
active_players = [p for p in room_data['players'] if p is not None]
active_spectators = [s for s in room_data['spectators'] if s is not None]
if len(active_players) == 0 and len(active_spectators) == 0:
rooms_to_remove.append(room_id)
continue
# Also remove rooms older than 24 hours with status 'waiting'
try:
created_at = datetime.fromisoformat(room_data['created_at'])
age_hours = (current_time - created_at).total_seconds() / 3600
if age_hours > 24 and room_data['game_data']['status'] == 'waiting':
rooms_to_remove.append(room_id)
except:
pass
for room_id in rooms_to_remove:
print(f'[SERVER] Cleaning up stale room: {room_id}')
if room_id in game_rooms:
del game_rooms[room_id]
if len(rooms_to_remove) > 0:
print(f'[SERVER] Cleaned up {len(rooms_to_remove)} stale rooms. Remaining: {len(game_rooms)}')
# Schedule next cleanup
Timer(300, cleanup_stale_rooms).start()
# Helper Functions (from original server.py)
def error(a):
return {'error': f'Invalid {a}'}
def key_move_obj_by_move_value_then_delta_remoteness(move_obj):
VALUES = (Value.WIN, Value.TIE, Value.DRAW, Value.LOSE, Value.UNSOLVED, Value.UNDECIDED)
move_value = move_obj['moveValue']
delta_remotenesss = move_obj['deltaRemoteness']
return (VALUES.index(move_value), delta_remotenesss)
def wrangle_move_objects_1Player(position_data):
if 'remoteness' not in position_data:
position_data['remoteness'] = Remoteness.INFINITY
current_position_remoteness = position_data['remoteness']
move_objs = position_data.get('moves', [])
for move_obj in move_objs:
if 'remoteness' not in move_obj:
move_obj['remoteness'] = Remoteness.INFINITY
move_obj['deltaRemoteness'] = 0
move_obj['moveValue'] = Value.LOSE
else:
delta_remoteness = current_position_remoteness - move_obj['remoteness']
move_obj['deltaRemoteness'] = delta_remoteness
move_obj['moveValue'] = Value.WIN if delta_remoteness > 0 else Value.LOSE if delta_remoteness < 0 else Value.TIE
move_objs.sort(key=key_move_obj_by_move_value_then_delta_remoteness)
def wrangle_move_objects_2Player(position_data):
if position_data['positionValue'] == Value.DRAW:
position_data['remoteness'] = Remoteness.INFINITY
move_objs = position_data.get('moves', [])
lose_children_remotenesses = []
win_children_remotenesses = []
tie_children_remotenesses = []
win_finite_unknown_child_remoteness_exists = False
lose_finite_unknown_child_remoteness_exists = False
tie_finite_unknown_child_remoteness_exists = False
for move_obj in move_objs:
child_value = move_obj['positionValue']
child_remoteness = move_obj.get('remoteness', Remoteness.INFINITY)
if child_value == Value.WIN:
if child_remoteness != Remoteness.FINITE_UNKNOWN:
win_children_remotenesses.append(child_remoteness)
else:
win_finite_unknown_child_remoteness_exists = True
elif child_value == Value.LOSE:
if child_remoteness != Remoteness.FINITE_UNKNOWN:
lose_children_remotenesses.append(child_remoteness)
else:
lose_finite_unknown_child_remoteness_exists = True
elif child_value == Value.TIE:
if child_remoteness != Remoteness.FINITE_UNKNOWN:
tie_children_remotenesses.append(child_remoteness)
else:
tie_finite_unknown_child_remoteness_exists = True
elif child_value == Value.DRAW:
move_obj['remoteness'] = child_remoteness
max_win_child_remoteness = max(win_children_remotenesses) if win_children_remotenesses else 0
if win_finite_unknown_child_remoteness_exists:
max_win_child_remoteness += 1
min_lose_child_remoteness = 1
max_lose_child_remoteness = 1
if lose_children_remotenesses:
min_lose_child_remoteness = min(lose_children_remotenesses)
if lose_finite_unknown_child_remoteness_exists:
max_lose_child_remoteness = max(lose_children_remotenesses) + 1
min_tie_child_remoteness = 1
max_tie_child_remoteness = 1
if tie_children_remotenesses:
min_tie_child_remoteness = min(tie_children_remotenesses)
if tie_finite_unknown_child_remoteness_exists:
max_tie_child_remoteness = max(tie_children_remotenesses) + 1
autogui_position = position_data['autoguiPosition']
not_in_autogui_format = not ((autogui_position[0] == '1' or autogui_position[0] == '2') and autogui_position[1] == '_')
for move_obj in move_objs:
position_value = move_obj['positionValue']
remoteness = move_obj['remoteness']
move_value = move_obj.get('moveValue', position_value)
if not_in_autogui_format or move_obj['autoguiPosition'][0] != autogui_position[0]:
if position_value == Value.WIN:
move_value = Value.LOSE
elif position_value == Value.LOSE:
move_value = Value.WIN
move_obj['moveValue'] = move_value
delta_remoteness = 0
if move_value == Value.WIN:
if remoteness == Remoteness.FINITE_UNKNOWN:
remoteness = max_lose_child_remoteness
delta_remoteness = remoteness - min_lose_child_remoteness
elif move_value == Value.LOSE:
if remoteness == Remoteness.FINITE_UNKNOWN:
remoteness = max_win_child_remoteness
delta_remoteness = max_win_child_remoteness - remoteness
elif move_value == Value.TIE:
if remoteness == Remoteness.FINITE_UNKNOWN:
remoteness = max_tie_child_remoteness
delta_remoteness = remoteness - min_tie_child_remoteness
move_obj['deltaRemoteness'] = delta_remoteness
move_objs.sort(key=key_move_obj_by_move_value_then_delta_remoteness)
# Regular HTTP Routes (same as before)
@app.route("/")
def get_games() -> list[dict[str, str]]:
all_games = [{
'id': game_id,
'name': game.name,
'type': 'twoPlayer' if game.is_two_player_game else 'onePlayer',
'gui': game.gui
} for game_id, game in games.items()]
all_games.sort(key=lambda g: g['name'])
return jsonify(all_games)
@app.route("/<game_id>/")
def get_game(game_id: str):
if game_id in games:
game = games[game_id]
return {
'id': game_id,
'name': game.name,
'variants': [
{'id': variant_id, 'name': variant.name, 'gui': variant.gui}
for variant_id, variant in game.variants.items()
],
'allowCustomVariantCreation': bool(game.custom_variant),
'supportsWinBy': game.supports_win_by
}
return error('Game')
@app.route('/<game_id>/<variant_id>/')
def get_variant(game_id: str, variant_id: str):
if game_id in games:
variant = games[game_id].variant(variant_id)
if variant:
start_position_data = variant.start_position()
return {
'id': variant_id,
'name': variant.name,
'startPosition': start_position_data.get('position', ''),
'autoguiStartPosition': start_position_data.get('autoguiPosition', ''),
'imageAutoGUIData': get_image_autogui_data(game_id, variant_id),
'gui': variant.gui
}
return error('Variant')
return error('Game')
@app.route('/<game_id>/<variant_id>/positions/')
def get_position(game_id: str, variant_id: str):
if game_id in games:
variant = games[game_id].variant(variant_id)
if variant:
position = request.args.get('p', None)
if position:
position_data = variant.position_data(position)
if position_data:
if games[game_id].is_two_player_game:
wrangle_move_objects_2Player(position_data)
else:
wrangle_move_objects_1Player(position_data)
return position_data
return error('Position')
return error('Variant')
return error('Game')
@app.route("/<game_id>/<variant_id>/instructions/")
def get_instructions(game_id: str, variant_id: str) -> dict[str: str]:
if game_id in games:
game_type = 'games' if games[game_id].is_two_player_game else 'puzzles'
locale = request.args.get('locale', 'en')
return {'instructions': md_instr(game_type, game_id, locale)}
return error('Game')
@app.route("/debug/rooms")
def debug_rooms():
"""Debug endpoint to see all active rooms"""
rooms_info = {}
for room_id, room_data in game_rooms.items():
rooms_info[room_id] = {
'players': room_data['players'],
'spectators': room_data['spectators'],
'status': room_data['game_data']['status'],
'is_public': room_data.get('is_public', False),
'created_at': room_data['created_at'],
'player_names': room_data['game_data']['playerNames']
}
return jsonify({
'total_rooms': len(game_rooms),
'total_connections': len(connections),
'rooms': rooms_info,
'connections': {sid: conn for sid, conn in connections.items()}
})
# SocketIO Events for Multiplayer
@socketio.on('connect')
def handle_connect():
print(f'Client connected: {request.sid}')
emit('connected', {'sid': request.sid})
def cleanup_room_if_empty(room_id):
"""Helper function to clean up empty rooms"""
if room_id in game_rooms:
room = game_rooms[room_id]
# Also check if players list contains only None values or is empty
active_players = [p for p in room['players'] if p is not None]
active_spectators = [s for s in room['spectators'] if s is not None]
if len(active_players) == 0 and len(active_spectators) == 0:
print(f'[SERVER] Cleaning up empty room: {room_id}')
del game_rooms[room_id]
return True
return False
@socketio.on('disconnect')
def handle_disconnect():
sid = request.sid
print(f'Client disconnected: {sid}')
if sid not in connections:
return
conn = connections[sid]
room_id = conn['room_id']
role = conn['role']
player_idx = conn.get('player_index')
# Remove from connections first
del connections[sid]
if room_id not in game_rooms:
return
room = game_rooms[room_id]
if role == 'player':
if player_idx is not None and player_idx < len(room['players']):
if room['players'][player_idx] == sid:
# Mark slot as None
room['players'][player_idx] = None
print(f'[SERVER] Player {player_idx} disconnected from room {room_id}, slot marked as None')
# Clear the player name
if player_idx < len(room['game_data']['playerNames']):
room['game_data']['playerNames'][player_idx] = None
# Update status if no active players
active_players = [p for p in room['players'] if p is not None]
if len(active_players) < 2:
room['game_data']['status'] = 'waiting'
print(f'[SERVER] Active players after disconnect: {active_players}')
print(f'[SERVER] Player names after disconnect: {room["game_data"]["playerNames"]}')
# Broadcast updated game data
emit('player_disconnected', {
'message': 'Player disconnected',
'playerIndex': player_idx,
'gameData': room['game_data']
}, room=room_id)
elif role == 'spectator':
if sid in room['spectators']:
room['spectators'].remove(sid)
print(f'[SERVER] Spectator disconnected from room {room_id}')
emit('spectator_left', {
'spectatorCount': len(room['spectators'])
}, room=room_id)
# Clean up empty rooms
cleanup_room_if_empty(room_id)
@socketio.on('create_room')
def handle_create_room(data):
"""Create a new game room"""
print(f'[SERVER] create_room event received from {request.sid}')
print(f'[SERVER] Room data: {data}')
room_id = secrets.token_urlsafe(8)
game_id = data.get('gameId')
variant_id = data.get('variantId')
player_name = data.get('playerName', 'Player 1')
is_public = data.get('isPublic', False) # Default to False if not specified
initial_state = data.get('initialState', {})
print(f'[SERVER] Creating room {room_id}, isPublic={is_public}')
move_history = initial_state.get('moveHistory', [])
current_turn = initial_state.get('currentTurn', len(move_history) % 2)
game_rooms[room_id] = {
'players': [request.sid],
'spectators': [],
'game_data': {
'gameId': game_id,
'variantId': variant_id,
'currentTurn': current_turn,
'moveHistory': move_history,
'playerNames': [player_name, None],
'status': 'waiting'
},
'created_at': datetime.now().isoformat(),
'is_public': is_public # Make sure this is set!
}
connections[request.sid] = {
'room_id': room_id,
'role': 'player',
'player_index': 0
}
join_room(room_id)
emit('room_created', {
'roomId': room_id,
'playerIndex': 0,
'gameData': game_rooms[room_id]['game_data']
})
print(f'[SERVER] Room created: {room_id} (public={is_public})')
@socketio.on('join_room')
def handle_join_room(data):
"""Join an existing game room"""
room_id = data.get('roomId')
player_name = data.get('playerName', 'Player 2')
as_spectator = data.get('asSpectator', False)
print(f'[SERVER] join_room event: roomId={room_id}, playerName={player_name}, asSpectator={as_spectator}, sid={request.sid}')
if room_id not in game_rooms:
print(f'[SERVER] Error: Room {room_id} not found')
emit('error', {'message': 'Room not found'})
return
room = game_rooms[room_id]
if as_spectator:
# Join as spectator
print(f'[SERVER] Adding {request.sid} as spectator to room {room_id}')
room['spectators'].append(request.sid)
connections[request.sid] = {
'room_id': room_id,
'role': 'spectator',
'player_name': player_name
}
join_room(room_id)
print(f'[SERVER] Emitting spectator_joined to {request.sid}')
emit('spectator_joined', {
'roomId': room_id,
'gameData': room['game_data'],
'spectatorCount': len(room['spectators'])
})
print(f'[SERVER] Broadcasting spectator count update to room {room_id}')
emit('spectator_joined', {
'spectatorCount': len(room['spectators'])
}, room=room_id, include_self=False)
print(f'[SERVER] Spectator joined room: {room_id}')
else:
# Join as player - assign to first available slot
# Ensure players list has exactly 2 slots
while len(room['players']) < 2:
room['players'].append(None)
# Ensure playerNames list has exactly 2 slots
while len(room['game_data']['playerNames']) < 2:
room['game_data']['playerNames'].append(None)
# Count active (non-None) players
active_players = [p for p in room['players'] if p is not None]
if len(active_players) >= 2:
# Room is full with 2 active players
print(f'[SERVER] Error: Room {room_id} is full (2 active players)')
emit('error', {'message': 'Room is full'})
return
# Assign to first available slot (prioritize slot 0, then slot 1)
player_index = None
if room['players'][0] is None:
room['players'][0] = request.sid
player_index = 0
print(f'[SERVER] Assigned to empty slot 0')
elif room['players'][1] is None:
room['players'][1] = request.sid
player_index = 1
print(f'[SERVER] Assigned to empty slot 1')
else:
# Both slots occupied (shouldn't reach here)
print(f'[SERVER] Error: Room {room_id} is full')
emit('error', {'message': 'Room is full'})
return
# Set the player name
room['game_data']['playerNames'][player_index] = player_name
# Only set status to active if both slots are filled with non-None values
active_players_after = [p for p in room['players'] if p is not None]
if len(active_players_after) >= 2:
room['game_data']['status'] = 'active'
connections[request.sid] = {
'room_id': room_id,
'role': 'player',
'player_index': player_index,
'player_name': player_name
}
join_room(room_id)
print(f'[SERVER] Player assigned to slot {player_index} in room {room_id}')
print(f'[SERVER] Current players: {room["players"]}')
print(f'[SERVER] Current player names: {room["game_data"]["playerNames"]}')
emit('room_joined', {
'roomId': room_id,
'playerIndex': player_index,
'gameData': room['game_data']
})
# Broadcast to others in room
emit('player_joined', {
'playerIndex': player_index,
'playerName': player_name,
'gameData': room['game_data']
}, room=room_id, include_self=False)
print(f'[SERVER] Player joined room: {room_id} as Player {player_index + 1}')
@socketio.on('make_move')
def handle_make_move(data):
"""Handle a player making a move"""
if request.sid not in connections:
emit('error', {'message': 'Not in a room'})
return
conn = connections[request.sid]
if conn['role'] != 'player':
emit('error', {'message': 'Spectators cannot make moves'})
return
room_id = conn['room_id']
player_index = conn['player_index']
if room_id not in game_rooms:
emit('error', {'message': 'Room not found'})
return
game_data = game_rooms[room_id]['game_data']
# Check if it's the player's turn
if game_data['currentTurn'] != player_index:
emit('error', {'message': 'Not your turn'})
return
# Process the move
move_data = {
'move': data.get('move'),
'autoguiMove': data.get('autoguiMove'),
'position': data.get('position'),
'playerIndex': player_index
}
game_data['moveHistory'].append(move_data)
game_data['currentTurn'] = 1 - player_index
# Broadcast move to all in room (players + spectators)
emit('move_made', {
'move': move_data,
'currentTurn': game_data['currentTurn'],
'gameData': game_data
}, room=room_id)
print(f'Move made in room {room_id}: {move_data["autoguiMove"]}')
@socketio.on('game_over')
def handle_game_over(data):
"""Handle game over event"""
if request.sid not in connections:
return
room_id = connections[request.sid]['room_id']
if room_id in game_rooms:
game_rooms[room_id]['game_data']['status'] = 'completed'
emit('game_over', {
'winner': data.get('winner'),
'reason': data.get('reason', 'Game completed')
}, room=room_id)
@socketio.on('leave_room')
def handle_leave_room():
"""Handle player/spectator leaving a room"""
sid = request.sid
print(f'[SERVER] leave_room called by {sid}')
if sid not in connections:
emit('left_room', {'success': False, 'message': 'Not in a room'})
return
conn = connections[sid]
room_id = conn['room_id']
role = conn['role']
player_idx = conn.get('player_index')
print(f'[SERVER] Leaving room {room_id}, role={role}, player_idx={player_idx}')
# Leave the socket.io room
leave_room(room_id)
# Remove from connections
del connections[sid]
if room_id not in game_rooms:
emit('left_room', {'success': True})
return
room = game_rooms[room_id]
if role == 'player':
if player_idx is not None and player_idx < len(room['players']):
if room['players'][player_idx] == sid:
# Mark slot as None
room['players'][player_idx] = None
print(f'[SERVER] Player {player_idx} left room {room_id}, slot marked as None')
# Clear the player name
if player_idx < len(room['game_data']['playerNames']):
room['game_data']['playerNames'][player_idx] = None
# Update game status back to waiting if less than 2 active players
active_players = [p for p in room['players'] if p is not None]
if len(active_players) < 2:
room['game_data']['status'] = 'waiting'
print(f'[SERVER] Active players after leave: {active_players}')
print(f'[SERVER] Player names after leave: {room["game_data"]["playerNames"]}')
# Broadcast updated game data to everyone
emit('player_left', {
'message': 'Player left the game',
'playerIndex': player_idx,
'gameData': room['game_data']
}, room=room_id)
elif role == 'spectator':
if sid in room['spectators']:
room['spectators'].remove(sid)
print(f'[SERVER] Spectator left room {room_id}')
emit('spectator_left', {
'spectatorCount': len(room['spectators'])
}, room=room_id)
# Clean up empty rooms
cleaned = cleanup_room_if_empty(room_id)
emit('left_room', {'success': True, 'roomCleaned': cleaned})
@socketio.on('chat_message')
def handle_chat_message(data):
"""Handle chat messages"""
print(f'[SERVER] 📨 chat_message event from {request.sid}')
print(f'[SERVER] Message content: {data.get("message")}')
if request.sid not in connections:
print(f'[SERVER] ❌ Unknown connection: {request.sid}')
print(f'[SERVER] Known connections: {list(connections.keys())}')
return
conn = connections[request.sid]
room_id = conn['room_id']
print(f'[SERVER] Connection role: {conn["role"]}, room: {room_id}')
if room_id not in game_rooms:
print(f'[SERVER] ❌ Room not found: {room_id}')
return
sender_name = 'Unknown'
sender_index = None
if conn['role'] == 'player':
player_index = conn['player_index']
sender_index = player_index
sender_name = conn.get('player_name')
if not sender_name:
player_names = game_rooms[room_id]['game_data']['playerNames']
if player_index < len(player_names) and player_names[player_index]:
sender_name = player_names[player_index]
else:
sender_name = f'Player {player_index + 1}'
else: # spectator
sender_name = conn.get('player_name', 'Spectator')
message_data = {
'senderRole': conn['role'],
'senderIndex': sender_index,
'senderName': sender_name,
'message': data.get('message'),
'timestamp': datetime.now().isoformat()
}
print(f'[SERVER] 📢 Broadcasting from {sender_name} ({conn["role"]}) to room {room_id}')
print(f'[SERVER] Full message data: {message_data}')
# Get all members of the room for debugging
room_members = []
for sid, c in connections.items():
if c.get('room_id') == room_id:
room_members.append(f"{sid[:8]}({c['role']})")
print(f'[SERVER] Room members: {room_members}')
# Broadcast to ALL users in the room (including self)
emit('chat_message', message_data, room=room_id, include_self=True)
print(f'[SERVER] ✅ Message broadcasted')
@socketio.on('list_public_rooms')
def handle_list_public_rooms(data=None):
"""List all public rooms"""
print(f'[SERVER] list_public_rooms event received from {request.sid}')
print(f'[SERVER] Total rooms: {len(game_rooms)}')
public_rooms = []
for room_id, room_data in game_rooms.items():
print(f'[SERVER] Room {room_id}: is_public={room_data.get("is_public", False)}')
if room_data.get('is_public', False):
game_data = room_data['game_data']
# Count actual active players (non-None)
active_players = [p for p in room_data['players'] if p is not None]
player_count = len(active_players)
# Filter player names to only show active players
active_player_names = [
name for i, name in enumerate(game_data['playerNames'])
if i < len(room_data['players']) and room_data['players'][i] is not None
]
public_rooms.append({
'roomId': room_id,
'gameId': game_data['gameId'],
'variantId': game_data['variantId'],
'playerNames': active_player_names, # Only active player names
'playerCount': player_count, # Actual count
'spectatorCount': len(room_data['spectators']),
'status': game_data['status'],
'createdAt': room_data['created_at']
})
print(f'[SERVER] Found {len(public_rooms)} public rooms')
print(f'[SERVER] Emitting public_rooms to {request.sid}')
emit('public_rooms', {'rooms': public_rooms})
print(f'[SERVER] Emitted public_rooms successfully')
if __name__ == '__main__':
Timer(300, cleanup_stale_rooms).start()
socketio.run(app, host='0.0.0.0', port=8082, debug=True)