import random import json import socketio from flask import Flask from redis.sentinel import Sentinel import eventlet eventlet.monkey_patch() # Redis Sentinel Setup SENTINELS = [('sentinel1', 26379), ('sentinel2', 26379), ('sentinel3', 26379)] MASTER_NAME = 'mymaster' sentinel = Sentinel(SENTINELS, socket_timeout=0.1) redis_master = sentinel.master_for(MASTER_NAME, socket_timeout=0.1, decode_responses=True) redis_slave = sentinel.slave_for(MASTER_NAME, socket_timeout=0.1, decode_responses=True) # Flask + Socket.IO setup sio = socketio.Server(cors_allowed_origins='*') app = Flask(__name__) app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app) # Redis keys for data storage WAITING_PLAYERS_KEY = 'waiting_players' # Redis List PLAYERS_HASH = 'players' # Redis Hash: sid -> json string ROOMS_HASH = 'rooms' # Redis Hash: sid -> room GAME_STATES_HASH = 'game_states' # Redis Hash: room -> json string # Pub/Sub channel for broadcasting moves and events CHANNEL = 'checkers_channel' def init_board(): board = [[None for _ in range(8)] for _ in range(8)] for row in range(3): for col in range(8): if (row + col) % 2 == 1: board[row][col] = 'b' for row in range(5, 8): for col in range(8): if (row + col) % 2 == 1: board[row][col] = 'r' return board def publish_event(event_type, data): message = json.dumps({'type': event_type, 'data': data}) redis_master.publish(CHANNEL, message) def handle_pubsub(): pubsub = redis_slave.pubsub() pubsub.subscribe(CHANNEL) for message in pubsub.listen(): if message['type'] == 'message': event = json.loads(message['data']) event_type = event['type'] data = event['data'] # Broadcast to all connected sockets as needed if event_type == 'opponent_move': room = data.get('room') if room: # Emit to the room, skip sender sio.emit('opponent_move', data['move'], room=room, skip_sid=data['sid']) elif event_type == 'game_over': sio.emit('game_over', data, room=data.get('room')) elif event_type == 'opponent_disconnected': sio.emit('opponent_disconnected', room=data.get('room')) elif event_type == 'match_found': # This can be handled if needed for multi-instance matchmaking pass @app.route('/') def index(): return "Checkers server with Redis Sentinel is running." @sio.event def connect(sid, environ): print(f"{sid} connected") @sio.event def set_name(sid, data): name = data.get('name', 'Player') # Store player info in Redis player_data = {'name': name} redis_master.hset(PLAYERS_HASH, sid, json.dumps(player_data)) # Check waiting players waiting = redis_master.lrange(WAITING_PLAYERS_KEY, 0, -1) if waiting: opponent_sid = waiting[0] redis_master.lpop(WAITING_PLAYERS_KEY) room = f"room_{sid[:4]}_{opponent_sid[:4]}" # Update players with room player_data['room'] = room redis_master.hset(PLAYERS_HASH, sid, json.dumps(player_data)) opponent_data_json = redis_master.hget(PLAYERS_HASH, opponent_sid) opponent_data = json.loads(opponent_data_json) opponent_data['room'] = room redis_master.hset(PLAYERS_HASH, opponent_sid, json.dumps(opponent_data)) redis_master.hset(ROOMS_HASH, sid, room) redis_master.hset(ROOMS_HASH, opponent_sid, room) sio.enter_room(sid, room) sio.enter_room(opponent_sid, room) # Random color assignment pair = [(sid, 'red'), (opponent_sid, 'black')] random.shuffle(pair) for psid, color in pair: pdata_json = redis_master.hget(PLAYERS_HASH, psid) pdata = json.loads(pdata_json) pdata['color'] = color redis_master.hset(PLAYERS_HASH, psid, json.dumps(pdata)) opponent_name = players_get_name(opponent_sid if psid == sid else sid) sio.emit("match_found", {'color': color, 'opponent': opponent_name}, to=psid) # Initialize game state in Redis state = { 'board': init_board(), 'turn': 'red' } redis_master.hset(GAME_STATES_HASH, room, json.dumps(state)) else: redis_master.rpush(WAITING_PLAYERS_KEY, sid) def players_get_name(sid): pdata_json = redis_slave.hget(PLAYERS_HASH, sid) if pdata_json: pdata = json.loads(pdata_json) return pdata.get('name', 'Player') return 'Player' @sio.event def move(sid, data): room = redis_slave.hget(ROOMS_HASH, sid) if not room: return state_json = redis_slave.hget(GAME_STATES_HASH, room) if not state_json: return state = json.loads(state_json) board = state['board'] turn = state['turn'] pdata_json = redis_slave.hget(PLAYERS_HASH, sid) if not pdata_json: return pdata = json.loads(pdata_json) color = pdata.get('color') if color != turn: return # not your turn from_row, from_col = data['from'] to_row, to_col = data['to'] captured = data.get('captured') piece = board[from_row][from_col] if not piece or piece[0] != color[0]: return # Simple move board[to_row][to_col] = board[from_row][from_col] board[from_row][from_col] = None if captured: cap_row, cap_col = captured board[cap_row][cap_col] = None # Crown if (color == 'red' and to_row == 0) or (color == 'black' and to_row == 7): board[to_row][to_col] = board[to_row][to_col].upper() # Win check flat = sum(board, []) winner = None if 'r' not in flat and 'R' not in flat: winner = 'black' elif 'b' not in flat and 'B' not in flat: winner = 'red' if winner: publish_event('game_over', {'winner': winner, 'room': room}) redis_master.hdel(GAME_STATES_HASH, room) return # Switch turn state['turn'] = 'black' if turn == 'red' else 'red' state['board'] = board redis_master.hset(GAME_STATES_HASH, room, json.dumps(state)) # Broadcast move to other player via Redis pubsub publish_event('opponent_move', {'move': data, 'room': room, 'sid': sid}) @sio.event def disconnect(sid): print(f"{sid} disconnected") # Remove from waiting list if present redis_master.lrem(WAITING_PLAYERS_KEY, 0, sid) room = redis_slave.hget(ROOMS_HASH, sid) if room: publish_event('opponent_disconnected', {'room': room}) # Clean up Redis data for this room & players keys_to_delete = [] players_in_room = [] all_players = redis_slave.hkeys(PLAYERS_HASH) for psid in all_players: pdata_json = redis_slave.hget(PLAYERS_HASH, psid) pdata = json.loads(pdata_json) if pdata.get('room') == room: keys_to_delete.append(psid) players_in_room.append(psid) for psid in players_in_room: redis_master.hdel(PLAYERS_HASH, psid) redis_master.hdel(ROOMS_HASH, psid) redis_master.hdel(GAME_STATES_HASH, room) if __name__ == '__main__': import threading # Start background thread for Redis Pub/Sub listening pubsub_thread = threading.Thread(target=handle_pubsub, daemon=True) pubsub_thread.start() eventlet.wsgi.server(eventlet.listen(('0.0.0.0', 5000)), app)