File size: 7,589 Bytes
22a71cf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
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)