jah242 commited on
Commit
88fee7d
·
verified ·
1 Parent(s): af01276

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +342 -0
app.py ADDED
@@ -0,0 +1,342 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ from flask import Flask, render_template, send_from_directory, request
3
+ from flask_socketio import SocketIO, emit
4
+ from collections import defaultdict
5
+ import os
6
+
7
+ app = Flask(__name__)
8
+ app.config['SECRET_KEY'] = 'your-secret-key'
9
+ socketio = SocketIO(app, cors_allowed_origins="*") # polling OK with Werkzeug
10
+
11
+ # Read password from Hugging Face Space secret (prefer 'password', fallback to 'PASSWORD')
12
+ SPACE_PASSWORD = os.environ.get('password') or os.environ.get('PASSWORD') or None
13
+
14
+ # --------------------
15
+ # Game state
16
+ # --------------------
17
+ class GoGame:
18
+ def __init__(self, size=13):
19
+ # (unchanged)
20
+ self.size = size
21
+ self.board = [[None for _ in range(size)] for _ in range(size)]
22
+ self.current_player = 'black'
23
+ self.captured = {'black': 0, 'white': 0} # stones captured OF that color
24
+ self.passes = 0
25
+ self.game_over = False
26
+ self.scores = {'black': 0, 'white': 0}
27
+ self.game_history = {'black': 0, 'white': 0} # per-color tally (kept for reference)
28
+ self.last_move = None
29
+
30
+ def place_stone(self, x, y, color):
31
+ if self.game_over:
32
+ return False
33
+ if not (0 <= x < self.size and 0 <= y < self.size):
34
+ return False
35
+ if self.board[x][y] is not None:
36
+ return False
37
+
38
+ self.board[x][y] = color
39
+ self.last_move = (x, y)
40
+
41
+ opponent = 'white' if color == 'black' else 'black'
42
+ removed = self.check_captures(x, y, opponent)
43
+ self.captured[opponent] += len(removed)
44
+
45
+ # suicide check (simplified)
46
+ if not self.has_liberties(x, y, color):
47
+ self.board[x][y] = None
48
+ return False
49
+
50
+ self.current_player = opponent
51
+ self.passes = 0
52
+ return True
53
+
54
+ def check_captures(self, x, y, opponent):
55
+ captured = []
56
+ for dx, dy in [(0,1), (1,0), (0,-1), (-1,0)]:
57
+ nx, ny = x + dx, y + dy
58
+ if 0 <= nx < self.size and 0 <= ny < self.size:
59
+ if self.board[nx][ny] == opponent and not self.has_liberties(nx, ny, opponent):
60
+ captured.extend(self.remove_group(nx, ny, opponent))
61
+ return captured
62
+
63
+ def has_liberties(self, x, y, color):
64
+ visited = set()
65
+ return self._has_liberties_recursive(x, y, color, visited)
66
+
67
+ def _has_liberties_recursive(self, x, y, color, visited):
68
+ if (x, y) in visited:
69
+ return False
70
+ visited.add((x, y))
71
+ for dx, dy in [(0,1), (1,0), (0,-1), (-1,0)]:
72
+ nx, ny = x + dx, y + dy
73
+ if 0 <= nx < self.size and 0 <= ny < self.size:
74
+ if self.board[nx][ny] is None:
75
+ return True
76
+ if self.board[nx][ny] == color and self._has_liberties_recursive(nx, ny, color, visited):
77
+ return True
78
+ return False
79
+
80
+ def remove_group(self, x, y, color):
81
+ visited = set()
82
+ self._remove_group_recursive(x, y, color, visited)
83
+ return list(visited)
84
+
85
+ def _remove_group_recursive(self, x, y, color, visited):
86
+ if (x, y) in visited:
87
+ return
88
+ if not (0 <= x < self.size and 0 <= y < self.size):
89
+ return
90
+ if self.board[x][y] != color:
91
+ return
92
+ visited.add((x, y))
93
+ self.board[x][y] = None
94
+ for dx, dy in [(0,1), (1,0), (0,-1), (-1,0)]:
95
+ self._remove_group_recursive(x + dx, y + dy, color, visited)
96
+
97
+ def pass_turn(self):
98
+ if self.game_over:
99
+ return
100
+ self.passes += 1
101
+ self.current_player = 'white' if self.current_player == 'black' else 'black'
102
+ if self.passes >= 2:
103
+ self.end_game()
104
+
105
+ def resign(self, player_color):
106
+ winner = 'white' if player_color == 'black' else 'black'
107
+ self.game_history[winner] += 1 # per-color history (kept)
108
+ self.game_over = True
109
+ return winner
110
+
111
+ def end_game(self):
112
+ # simple scoring: stones on board + captured; komi for white
113
+ black_score = self.captured['black']
114
+ white_score = self.captured['white'] + 6.5
115
+ for row in self.board:
116
+ for cell in row:
117
+ if cell == 'black':
118
+ black_score += 1
119
+ elif cell == 'white':
120
+ white_score += 1
121
+ self.scores['black'] = int(black_score)
122
+ self.scores['white'] = int(white_score)
123
+ if black_score > white_score:
124
+ self.game_history['black'] += 1
125
+ else:
126
+ self.game_history['white'] += 1
127
+ self.game_over = True
128
+
129
+ def reset(self, keep_history=True):
130
+ """Reset board/state; preserve per-color history by default."""
131
+ hist = self.game_history if keep_history else {'black': 0, 'white': 0}
132
+ self.board = [[None for _ in range(self.size)] for _ in range(self.size)]
133
+ self.current_player = 'black'
134
+ self.captured = {'black': 0, 'white': 0}
135
+ self.passes = 0
136
+ self.game_over = False
137
+ self.scores = {'black': 0, 'white': 0}
138
+ self.last_move = None
139
+ self.game_history = hist
140
+
141
+ # --------------------
142
+ # Globals
143
+ # --------------------
144
+ game = GoGame(size=13)
145
+ sid_to_username = {} # sid -> username
146
+ current_player_user = {'black': None, 'white': None} # color -> username
147
+ wins_by_user = defaultdict(int) # username -> wins
148
+
149
+ # --------------------
150
+ # Helpers
151
+ # --------------------
152
+ def winner_username_for_color(color):
153
+ return current_player_user.get(color)
154
+
155
+ def snapshot():
156
+ return {
157
+ 'board_size': game.size,
158
+ 'current_player': game.current_player,
159
+ 'scores': game.scores,
160
+ 'game_history': game.game_history, # per-color (legacy)
161
+ 'wins_by_user': dict(wins_by_user), # username-based (authoritative for UI)
162
+ 'board': game.board,
163
+ 'last_move': game.last_move,
164
+ 'captured': game.captured,
165
+ 'passes': game.passes,
166
+ 'game_over': game.game_over
167
+ }
168
+
169
+ def broadcast_colors():
170
+ socketio.emit('colors', {
171
+ 'black': current_player_user['black'],
172
+ 'white': current_player_user['white']
173
+ })
174
+
175
+ def require_auth():
176
+ """Ensure the current socket is authenticated via successful join."""
177
+ if request.sid not in sid_to_username:
178
+ emit('error', {'message': 'Not authenticated'})
179
+ return False
180
+ return True
181
+
182
+ # --------------------
183
+ # HTTP
184
+ # --------------------
185
+ @app.route('/')
186
+ def index():
187
+ return render_template('index.html')
188
+
189
+ @app.route('/<path:path>')
190
+ def static_files(path):
191
+ return send_from_directory('.', path)
192
+
193
+ # --------------------
194
+ # Socket events
195
+ # --------------------
196
+ @socketio.on('connect')
197
+ def on_connect():
198
+ print('Client connected', request.sid)
199
+
200
+ @socketio.on('disconnect')
201
+ def on_disconnect():
202
+ sid = request.sid
203
+ username = sid_to_username.pop(sid, None)
204
+ print('Client disconnected', sid, username)
205
+ # Do not clear color mapping on disconnect; persists until new_game.
206
+
207
+ @socketio.on('join')
208
+ def on_join(data):
209
+ username = (data or {}).get('username')
210
+ provided_password = (data or {}).get('password')
211
+ # Enforce password if configured
212
+ if SPACE_PASSWORD and provided_password != SPACE_PASSWORD:
213
+ emit('error', {'message': 'Invalid password'})
214
+ return
215
+ if not username:
216
+ emit('error', {'message': 'Username required'})
217
+ return
218
+ sid_to_username[request.sid] = username
219
+ emit('init', snapshot())
220
+ broadcast_colors()
221
+
222
+ @socketio.on('claim_color')
223
+ def on_claim_color(data):
224
+ if not require_auth():
225
+ return
226
+ username = (data or {}).get('username')
227
+ color = (data or {}).get('color')
228
+ if color not in ('black', 'white'):
229
+ emit('error', {'message': 'Bad color'})
230
+ return
231
+ other = 'white' if color == 'black' else 'black'
232
+
233
+ # user cannot hold both colors
234
+ if current_player_user.get(other) == username:
235
+ emit('error', {'message': 'You already claimed the other color'})
236
+ return
237
+
238
+ # idempotent
239
+ if current_player_user.get(color) == username:
240
+ broadcast_colors()
241
+ return
242
+
243
+ # claim if free
244
+ if current_player_user.get(color) is None:
245
+ current_player_user[color] = username
246
+ broadcast_colors()
247
+ else:
248
+ emit('error', {'message': f'{color} already taken'})
249
+
250
+ @socketio.on('move')
251
+ def on_move(data):
252
+ if not require_auth():
253
+ return
254
+ x = int((data or {}).get('x', -1))
255
+ y = int((data or {}).get('y', -1))
256
+ user = (data or {}).get('player')
257
+
258
+ moving_color = game.current_player
259
+ if user != current_player_user.get(moving_color):
260
+ emit('error', {'message': 'Not your turn!'})
261
+ return
262
+
263
+ if game.place_stone(x, y, moving_color):
264
+ socketio.emit('move', {
265
+ 'x': x,
266
+ 'y': y,
267
+ 'player': moving_color,
268
+ 'next_player': game.current_player,
269
+ 'captured': game.captured
270
+ })
271
+ else:
272
+ emit('error', {'message': 'Invalid move!'})
273
+
274
+ @socketio.on('pass')
275
+ def on_pass(data):
276
+ if not require_auth():
277
+ return
278
+ user = (data or {}).get('player')
279
+ if user != current_player_user.get(game.current_player):
280
+ emit('error', {'message': 'Not your turn!'})
281
+ return
282
+
283
+ game.pass_turn()
284
+ socketio.emit('pass', {'next_player': game.current_player})
285
+
286
+ if game.game_over:
287
+ # award win to username (if not a draw)
288
+ if game.scores['black'] != game.scores['white']:
289
+ winner_color = 'black' if game.scores['black'] > game.scores['white'] else 'white'
290
+ wuser = winner_username_for_color(winner_color)
291
+ if wuser:
292
+ wins_by_user[wuser] += 1
293
+
294
+ socketio.emit('game_over', {
295
+ 'scores': game.scores,
296
+ 'game_history': game.game_history,
297
+ 'wins_by_user': dict(wins_by_user)
298
+ })
299
+
300
+ @socketio.on('resign')
301
+ def on_resign(data):
302
+ if not require_auth():
303
+ return
304
+ user = (data or {}).get('player')
305
+ # find resigning player's color
306
+ player_color = None
307
+ for color, uname in current_player_user.items():
308
+ if uname == user:
309
+ player_color = color
310
+ break
311
+ if not player_color:
312
+ emit('error', {'message': 'You have not claimed a color'})
313
+ return
314
+
315
+ winner_color = game.resign(player_color)
316
+ wuser = winner_username_for_color(winner_color)
317
+ if wuser:
318
+ wins_by_user[wuser] += 1
319
+
320
+ socketio.emit('resign', {
321
+ 'winner': winner_color,
322
+ 'scores': game.scores,
323
+ 'game_history': game.game_history,
324
+ 'wins_by_user': dict(wins_by_user)
325
+ })
326
+
327
+ @socketio.on('new_game')
328
+ def on_new_game(data):
329
+ if not require_auth():
330
+ return
331
+ # reset board; keep username win history and per-color history
332
+ game.reset(keep_history=True)
333
+ current_player_user['black'] = None
334
+ current_player_user['white'] = None
335
+ socketio.emit('init', snapshot())
336
+ broadcast_colors()
337
+
338
+ # --------------------
339
+ # Run
340
+ # --------------------
341
+ if __name__ == '__main__':
342
+ socketio.run(app, host='0.0.0.0', port=int(os.environ.get('PORT', 7860)), allow_unsafe_werkzeug=True)