Geek7 commited on
Commit
e4fcb9e
·
verified ·
1 Parent(s): c31e07b

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +233 -0
  2. docker-compose.yml +74 -0
app.py ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ import json
3
+ import socketio
4
+ from flask import Flask
5
+ from redis.sentinel import Sentinel
6
+ import eventlet
7
+ eventlet.monkey_patch()
8
+
9
+ # Redis Sentinel Setup
10
+ SENTINELS = [('sentinel1', 26379), ('sentinel2', 26379), ('sentinel3', 26379)]
11
+ MASTER_NAME = 'mymaster'
12
+
13
+ sentinel = Sentinel(SENTINELS, socket_timeout=0.1)
14
+ redis_master = sentinel.master_for(MASTER_NAME, socket_timeout=0.1, decode_responses=True)
15
+ redis_slave = sentinel.slave_for(MASTER_NAME, socket_timeout=0.1, decode_responses=True)
16
+
17
+ # Flask + Socket.IO setup
18
+ sio = socketio.Server(cors_allowed_origins='*')
19
+ app = Flask(__name__)
20
+ app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app)
21
+
22
+ # Redis keys for data storage
23
+ WAITING_PLAYERS_KEY = 'waiting_players' # Redis List
24
+ PLAYERS_HASH = 'players' # Redis Hash: sid -> json string
25
+ ROOMS_HASH = 'rooms' # Redis Hash: sid -> room
26
+ GAME_STATES_HASH = 'game_states' # Redis Hash: room -> json string
27
+
28
+ # Pub/Sub channel for broadcasting moves and events
29
+ CHANNEL = 'checkers_channel'
30
+
31
+ def init_board():
32
+ board = [[None for _ in range(8)] for _ in range(8)]
33
+ for row in range(3):
34
+ for col in range(8):
35
+ if (row + col) % 2 == 1:
36
+ board[row][col] = 'b'
37
+ for row in range(5, 8):
38
+ for col in range(8):
39
+ if (row + col) % 2 == 1:
40
+ board[row][col] = 'r'
41
+ return board
42
+
43
+ def publish_event(event_type, data):
44
+ message = json.dumps({'type': event_type, 'data': data})
45
+ redis_master.publish(CHANNEL, message)
46
+
47
+ def handle_pubsub():
48
+ pubsub = redis_slave.pubsub()
49
+ pubsub.subscribe(CHANNEL)
50
+ for message in pubsub.listen():
51
+ if message['type'] == 'message':
52
+ event = json.loads(message['data'])
53
+ event_type = event['type']
54
+ data = event['data']
55
+
56
+ # Broadcast to all connected sockets as needed
57
+ if event_type == 'opponent_move':
58
+ room = data.get('room')
59
+ if room:
60
+ # Emit to the room, skip sender
61
+ sio.emit('opponent_move', data['move'], room=room, skip_sid=data['sid'])
62
+ elif event_type == 'game_over':
63
+ sio.emit('game_over', data, room=data.get('room'))
64
+ elif event_type == 'opponent_disconnected':
65
+ sio.emit('opponent_disconnected', room=data.get('room'))
66
+ elif event_type == 'match_found':
67
+ # This can be handled if needed for multi-instance matchmaking
68
+ pass
69
+
70
+ @app.route('/')
71
+ def index():
72
+ return "Checkers server with Redis Sentinel is running."
73
+
74
+ @sio.event
75
+ def connect(sid, environ):
76
+ print(f"{sid} connected")
77
+
78
+ @sio.event
79
+ def set_name(sid, data):
80
+ name = data.get('name', 'Player')
81
+
82
+ # Store player info in Redis
83
+ player_data = {'name': name}
84
+ redis_master.hset(PLAYERS_HASH, sid, json.dumps(player_data))
85
+
86
+ # Check waiting players
87
+ waiting = redis_master.lrange(WAITING_PLAYERS_KEY, 0, -1)
88
+ if waiting:
89
+ opponent_sid = waiting[0]
90
+ redis_master.lpop(WAITING_PLAYERS_KEY)
91
+
92
+ room = f"room_{sid[:4]}_{opponent_sid[:4]}"
93
+
94
+ # Update players with room
95
+ player_data['room'] = room
96
+ redis_master.hset(PLAYERS_HASH, sid, json.dumps(player_data))
97
+
98
+ opponent_data_json = redis_master.hget(PLAYERS_HASH, opponent_sid)
99
+ opponent_data = json.loads(opponent_data_json)
100
+ opponent_data['room'] = room
101
+ redis_master.hset(PLAYERS_HASH, opponent_sid, json.dumps(opponent_data))
102
+
103
+ redis_master.hset(ROOMS_HASH, sid, room)
104
+ redis_master.hset(ROOMS_HASH, opponent_sid, room)
105
+
106
+ sio.enter_room(sid, room)
107
+ sio.enter_room(opponent_sid, room)
108
+
109
+ # Random color assignment
110
+ pair = [(sid, 'red'), (opponent_sid, 'black')]
111
+ random.shuffle(pair)
112
+
113
+ for psid, color in pair:
114
+ pdata_json = redis_master.hget(PLAYERS_HASH, psid)
115
+ pdata = json.loads(pdata_json)
116
+ pdata['color'] = color
117
+ redis_master.hset(PLAYERS_HASH, psid, json.dumps(pdata))
118
+
119
+ opponent_name = players_get_name(opponent_sid if psid == sid else sid)
120
+ sio.emit("match_found", {'color': color, 'opponent': opponent_name}, to=psid)
121
+
122
+ # Initialize game state in Redis
123
+ state = {
124
+ 'board': init_board(),
125
+ 'turn': 'red'
126
+ }
127
+ redis_master.hset(GAME_STATES_HASH, room, json.dumps(state))
128
+ else:
129
+ redis_master.rpush(WAITING_PLAYERS_KEY, sid)
130
+
131
+ def players_get_name(sid):
132
+ pdata_json = redis_slave.hget(PLAYERS_HASH, sid)
133
+ if pdata_json:
134
+ pdata = json.loads(pdata_json)
135
+ return pdata.get('name', 'Player')
136
+ return 'Player'
137
+
138
+ @sio.event
139
+ def move(sid, data):
140
+ room = redis_slave.hget(ROOMS_HASH, sid)
141
+ if not room:
142
+ return
143
+ state_json = redis_slave.hget(GAME_STATES_HASH, room)
144
+ if not state_json:
145
+ return
146
+ state = json.loads(state_json)
147
+
148
+ board = state['board']
149
+ turn = state['turn']
150
+
151
+ pdata_json = redis_slave.hget(PLAYERS_HASH, sid)
152
+ if not pdata_json:
153
+ return
154
+ pdata = json.loads(pdata_json)
155
+ color = pdata.get('color')
156
+
157
+ if color != turn:
158
+ return # not your turn
159
+
160
+ from_row, from_col = data['from']
161
+ to_row, to_col = data['to']
162
+ captured = data.get('captured')
163
+
164
+ piece = board[from_row][from_col]
165
+ if not piece or piece[0] != color[0]:
166
+ return
167
+
168
+ # Simple move
169
+ board[to_row][to_col] = board[from_row][from_col]
170
+ board[from_row][from_col] = None
171
+ if captured:
172
+ cap_row, cap_col = captured
173
+ board[cap_row][cap_col] = None
174
+
175
+ # Crown
176
+ if (color == 'red' and to_row == 0) or (color == 'black' and to_row == 7):
177
+ board[to_row][to_col] = board[to_row][to_col].upper()
178
+
179
+ # Win check
180
+ flat = sum(board, [])
181
+ winner = None
182
+ if 'r' not in flat and 'R' not in flat:
183
+ winner = 'black'
184
+ elif 'b' not in flat and 'B' not in flat:
185
+ winner = 'red'
186
+
187
+ if winner:
188
+ publish_event('game_over', {'winner': winner, 'room': room})
189
+ redis_master.hdel(GAME_STATES_HASH, room)
190
+ return
191
+
192
+ # Switch turn
193
+ state['turn'] = 'black' if turn == 'red' else 'red'
194
+ state['board'] = board
195
+
196
+ redis_master.hset(GAME_STATES_HASH, room, json.dumps(state))
197
+
198
+ # Broadcast move to other player via Redis pubsub
199
+ publish_event('opponent_move', {'move': data, 'room': room, 'sid': sid})
200
+
201
+ @sio.event
202
+ def disconnect(sid):
203
+ print(f"{sid} disconnected")
204
+ # Remove from waiting list if present
205
+ redis_master.lrem(WAITING_PLAYERS_KEY, 0, sid)
206
+
207
+ room = redis_slave.hget(ROOMS_HASH, sid)
208
+ if room:
209
+ publish_event('opponent_disconnected', {'room': room})
210
+
211
+ # Clean up Redis data for this room & players
212
+ keys_to_delete = []
213
+ players_in_room = []
214
+ all_players = redis_slave.hkeys(PLAYERS_HASH)
215
+ for psid in all_players:
216
+ pdata_json = redis_slave.hget(PLAYERS_HASH, psid)
217
+ pdata = json.loads(pdata_json)
218
+ if pdata.get('room') == room:
219
+ keys_to_delete.append(psid)
220
+ players_in_room.append(psid)
221
+
222
+ for psid in players_in_room:
223
+ redis_master.hdel(PLAYERS_HASH, psid)
224
+ redis_master.hdel(ROOMS_HASH, psid)
225
+ redis_master.hdel(GAME_STATES_HASH, room)
226
+
227
+ if __name__ == '__main__':
228
+ import threading
229
+ # Start background thread for Redis Pub/Sub listening
230
+ pubsub_thread = threading.Thread(target=handle_pubsub, daemon=True)
231
+ pubsub_thread.start()
232
+
233
+ eventlet.wsgi.server(eventlet.listen(('0.0.0.0', 5000)), app)
docker-compose.yml ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ redis-master:
5
+ image: redis:6.2
6
+ container_name: redis-master
7
+ ports:
8
+ - "6379:6379"
9
+ command: redis-server --requirepass yourpassword
10
+ networks:
11
+ - redisnet
12
+
13
+ redis-replica:
14
+ image: redis:6.2
15
+ container_name: redis-replica
16
+ depends_on:
17
+ - redis-master
18
+ command: >
19
+ sh -c "redis-server --replicaof redis-master 6379 --requirepass yourpassword --masterauth yourpassword"
20
+ networks:
21
+ - redisnet
22
+
23
+ sentinel1:
24
+ image: redis:6.2
25
+ container_name: sentinel1
26
+ depends_on:
27
+ - redis-master
28
+ volumes:
29
+ - ./sentinel/sentinel1.conf:/etc/sentinel.conf
30
+ command: redis-server /etc/sentinel.conf --sentinel
31
+ ports:
32
+ - "26379:26379"
33
+ networks:
34
+ - redisnet
35
+
36
+ sentinel2:
37
+ image: redis:6.2
38
+ container_name: sentinel2
39
+ depends_on:
40
+ - redis-master
41
+ volumes:
42
+ - ./sentinel/sentinel2.conf:/etc/sentinel.conf
43
+ command: redis-server /etc/sentinel.conf --sentinel
44
+ ports:
45
+ - "26380:26379"
46
+ networks:
47
+ - redisnet
48
+
49
+ sentinel3:
50
+ image: redis:6.2
51
+ container_name: sentinel3
52
+ depends_on:
53
+ - redis-master
54
+ volumes:
55
+ - ./sentinel/sentinel3.conf:/etc/sentinel.conf
56
+ command: redis-server /etc/sentinel.conf --sentinel
57
+ ports:
58
+ - "26381:26379"
59
+ networks:
60
+ - redisnet
61
+
62
+ app:
63
+ build: .
64
+ depends_on:
65
+ - sentinel1
66
+ - sentinel2
67
+ - sentinel3
68
+ ports:
69
+ - "5000:5000"
70
+ networks:
71
+ - redisnet
72
+
73
+ networks:
74
+ redisnet: