sodastar commited on
Commit
8039f1f
·
1 Parent(s): 628a050

update app.py

Browse files
Files changed (1) hide show
  1. app.py +153 -128
app.py CHANGED
@@ -55,20 +55,27 @@ current_player = 1 # 1: Human (White), -1: AI (Black)
55
  last_move_coords = None
56
  board_size = 8
57
 
 
 
 
 
 
 
 
 
 
 
 
58
  def init_game_and_ai(n):
59
  """根据板子大小初始化游戏和 AI 模块"""
60
- global game, nnet, mcts, board_size
61
  board_size = n
62
  log.info(f"Initializing game and AI for {n}x{n} board.")
63
  game = OthelloGame(n) #
64
-
65
- # 注意:AlphaZero 模型训练通常针对固定尺寸。
66
- # 如果你的模型只支持 8x8,这里需要进行处理或重新训练。
67
- # 这里我们假设模型支持当前尺寸 n。
68
-
69
  # 重新配置 MCTS 参数用于 Play 模式
70
  play_args = dotdict({
71
- 'numMCTSSims': 400, # 对战时使用更多的模拟次数
72
  'cpuct': 1.0,
73
  'cuda': args.cuda # 继承 CUDA 设置
74
  })
@@ -85,6 +92,13 @@ def init_game_and_ai(n):
85
 
86
  mcts = MCTS(game, nnet, play_args) #
87
 
 
 
 
 
 
 
 
88
 
89
  def get_api_moves(board, player):
90
  """将 getValidMoves 结果从向量转换为 {x, y} 列表"""
@@ -103,154 +117,87 @@ def get_api_moves(board, player):
103
  def check_game_end(board, player):
104
  """检查游戏是否结束,并返回状态信息,基于绝对的棋子数量差异。"""
105
 
106
- # 获取游戏结束的相对结果 (1: player 赢, -1: player 输, 0: 平局)
107
- # 注意:这个结果是相对于传入的 player 而言的
108
  result = game.getGameEnded(board, player) #
109
 
110
  status = 'Ongoing'
111
  score_diff = 0
112
 
113
  if result is not None:
114
- # 获取白棋 (1) 和黑棋 (-1) 的绝对分数。
115
- # 这里的 score_diff 是:(白棋数量 - 黑棋数量)
116
  white_count = np.sum(board == 1)
117
  black_count = np.sum(board == -1)
118
  score_diff = int(white_count - black_count)
119
 
120
- if result == 0:
121
  status = f"Game Over: Draw. Score: {white_count} vs {black_count}"
122
  elif score_diff > 0:
123
- # 白棋 (Human) 数量多,人赢
124
  status = f"Game Over: Human (O) Wins! Score: {white_count} vs {black_count}"
125
  elif score_diff < 0:
126
- # 黑棋 (AI) 数量多,AI 赢
127
  status = f"Game Over: AI (X) Wins! Score: {white_count} vs {black_count}"
128
  else:
129
- # 理论上 result != 0 时分数不会为 0,但以防万一
130
  status = f"Game Over: Draw. Score: {white_count} vs {black_count}"
131
 
132
  return status
133
 
134
  @app.route('/api/game/new', methods=['POST'])
135
  def new_game():
136
- global current_board, current_player, last_move_coords, board_size
137
  data = request.json
138
  size = data.get('size', 8)
139
 
140
- # 【新增代码】接收 first_player 参数,默认为 1 (Human)
141
  first_player = data.get('first_player', 1)
 
142
 
 
 
143
  if game is None or size != board_size:
144
  init_game_and_ai(size)
145
-
146
- current_board = game.getInitBoard() #
147
- current_player = first_player # 【修改】使用接收到的 first_player 设置当前玩家
148
- last_move_coords = None
149
-
150
- status = check_game_end(current_board, current_player)
151
-
152
- # 【新增逻辑】如果 AI 先手,立即触发 AI 移动
153
- if current_player == -1 and status == 'Ongoing':
154
- return ai_move_logic() # 直接调用 AI 逻辑并返回结果
155
- # 对current_board进行flip,以符合前端显示习惯
156
- current_board = np.flip(current_board, 0)
157
-
158
- return jsonify({
159
- 'board': current_board.tolist(),
160
- 'legal_moves': get_api_moves(current_board, current_player),
161
- 'current_player': current_player,
162
- 'last_move': last_move_coords,
163
- 'status': status,
164
- })
165
 
 
 
 
 
166
 
167
- # @app.route('/api/game/human_move', methods=['POST'])
168
- # def human_move():
169
- # """处理人类玩家移动"""
170
- # global current_board, current_player, last_move_coords
171
-
172
- # if current_player != 1 or check_game_end(current_board, current_player) != 'Ongoing':
173
- # return jsonify({'error': 'Not your turn or game is over'}), 400
174
-
175
- # data = request.json
176
- # x = data.get('x')
177
- # y = data.get('y')
178
-
179
- # if x is None or y is None:
180
- # # 检查是否是 Pass 动作
181
- # if data.get('action') == 'pass':
182
- # action = game.n * game.n # Pass action is the last index
183
- # else:
184
- # return jsonify({'error': 'Invalid move coordinates'}), 400
185
- # else:
186
- # action = game.n * x + y
187
 
188
- # valids = game.getValidMoves(current_board, 1) #
189
- # if valids[action] == 0:
190
- # return jsonify({'error': 'Illegal move'}), 400
 
 
 
191
 
192
- # current_board, current_player = game.getNextState(current_board, 1, action) #
193
-
194
- # if action != game.n * game.n:
195
- # last_move_coords = {'x': x, 'y': y}
196
-
197
- # status = check_game_end(current_board, current_player)
198
-
199
- # # 如果游戏未结束且轮到 AI (-1)
200
- # if status == 'Ongoing' and current_player == -1:
201
- # # 在这里触发 AI 移动
202
- # return ai_move_logic()
203
-
204
- # return jsonify({
205
- # 'board': current_board.tolist(),
206
- # 'legal_moves': get_api_moves(current_board, current_player),
207
- # 'current_player': current_player,
208
- # 'last_move': last_move_coords,
209
- # 'status': status,
210
- # })
211
-
212
- def ai_move_logic():
213
- """AI 移动的逻辑封装,在 human_move 中调用"""
214
- global current_board, current_player, last_move_coords
215
-
216
- canonical_board = game.getCanonicalForm(current_board, -1) #
217
-
218
- # 获取 AI 的最佳动作 (temp=0)
219
- ai_action = np.argmax(mcts.getActionProb(canonical_board, temp=0)) #
220
-
221
- # 更新游戏状态
222
- current_board, next_player = game.getNextState(current_board, -1, ai_action) #
223
- current_player = next_player
224
-
225
- # 记录 AI 的移动坐标
226
- if ai_action != game.n * game.n: # 如果不是 Pass 动作
227
- ai_x = ai_action // game.n
228
- ai_y = ai_action % game.n
229
- last_move_coords = {'x': int(ai_x), 'y': int(ai_y)}
230
-
231
  status = check_game_end(current_board, current_player)
232
 
233
- # 对current_board进行flip,以符合前端显示习惯
234
  current_board = np.flip(current_board, 0)
235
 
 
236
  return jsonify({
237
  'board': current_board.tolist(),
238
  'legal_moves': get_api_moves(current_board, current_player),
239
  'current_player': current_player,
240
  'last_move': last_move_coords,
241
  'status': status,
 
242
  })
243
 
244
- # app.py (在 @app.route('/api/game/human_move', methods=['POST']) 路由下)
245
- # 替换原有的 handleHumanMove/human_move 函数
246
 
247
  @app.route('/api/game/human_move', methods=['POST'])
248
  def human_move():
249
- """处理人类玩家移动,并返回给 AI 的中间状态"""
250
  global current_board, current_player, last_move_coords
251
 
252
  if current_player != 1 or check_game_end(current_board, current_player) != 'Ongoing':
253
- return jsonify({'error': 'Not your turn or game is over'}), 400
254
 
255
  data = request.json
256
  x = data.get('x')
@@ -261,76 +208,154 @@ def human_move():
261
  if data.get('action') == 'pass':
262
  action = game.n * game.n # Pass action is the last index
263
  else:
264
- return jsonify({'error': 'Invalid move coordinates'}), 400
265
  else:
266
  action = game.n * x + y
267
 
268
  valids = game.getValidMoves(current_board, 1)
269
  if valids[action] == 0:
270
- return jsonify({'error': 'Illegal move'}), 400
271
 
272
- # 执行人类移动
273
  current_board, current_player = game.getNextState(current_board, 1, action)
274
 
 
 
 
275
  if action != game.n * game.n:
276
  last_move_coords = {'x': x, 'y': y}
 
 
277
 
278
  status = check_game_end(current_board, current_player)
279
 
280
- # 对current_board进行flip,以符合前端显示习惯
281
  # current_board = np.flip(current_board, 0)
282
 
283
- # 注意:这里不再包含 AI 移动逻辑,直接返回
284
  return jsonify({
285
- 'board': current_board.tolist(),
286
  'legal_moves': get_api_moves(current_board, current_player),
287
  'current_player': current_player,
288
  'last_move': last_move_coords,
289
  'status': status,
 
290
  })
291
 
292
 
293
- # B. 新增 `ai_move` 路由
294
-
295
- @app.route('/api/game/ai_move', methods=['POST'])
296
- def ai_move():
297
- start_time = time.time()
298
- """触发 AI 移动,并返回最终状态"""
299
  global current_board, current_player, last_move_coords
300
 
301
- if current_player != -1:
302
- return jsonify({'error': 'Not AI turn'}), 400
303
-
304
-
305
- canonical_board = game.getCanonicalForm(current_board, -1)
306
- ai_action = np.argmax(mcts.getActionProb(canonical_board, temp=0))
307
-
308
- # 执行 AI 移动
309
- current_board, next_player = game.getNextState(current_board, -1, ai_action)
310
  current_player = next_player
311
 
312
  # 记录 AI 的移动坐标
313
- if ai_action != game.n * game.n:
314
  ai_x = ai_action // game.n
315
  ai_y = ai_action % game.n
316
  last_move_coords = {'x': int(ai_x), 'y': int(ai_y)}
317
  else:
318
  last_move_coords = None # AI Pass
319
-
320
  status = check_game_end(current_board, current_player)
321
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  # 控制 AI 最少思考时间为 0.5 秒
323
  end_time = time.time()
324
  used_time = end_time - start_time
325
  if used_time < 0.5:
326
  time.sleep(0.5 - used_time) # 确保至少等待0.5秒
327
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
  return jsonify({
329
  'board': current_board.tolist(),
330
  'legal_moves': get_api_moves(current_board, current_player),
331
  'current_player': current_player,
332
  'last_move': last_move_coords,
333
  'status': status,
 
334
  })
335
 
336
  if __name__ == '__main__':
@@ -340,4 +365,4 @@ if __name__ == '__main__':
340
 
341
  port = int(os.environ.get('PORT', 7860))
342
  # ... (日志) ...
343
- app.run(host='0.0.0.0', port=port)
 
55
  last_move_coords = None
56
  board_size = 8
57
 
58
+ history_stack = []
59
+ first_player_game = 1
60
+
61
+ # 【新增】辅助函数:保存当前状态到历史栈
62
+ def save_state():
63
+ """保存当前棋盘的副本和当前轮到的玩家到历史栈。"""
64
+ global current_board, current_player, history_stack
65
+ # 保存当前棋盘的副本和当前轮到的玩家
66
+ # 注意:这里保存的是未翻转的内部游戏状态
67
+ history_stack.append((np.copy(current_board), current_player))
68
+
69
  def init_game_and_ai(n):
70
  """根据板子大小初始化游戏和 AI 模块"""
71
+ global game, nnet, mcts, board_size, history_stack, current_board
72
  board_size = n
73
  log.info(f"Initializing game and AI for {n}x{n} board.")
74
  game = OthelloGame(n) #
75
+
 
 
 
 
76
  # 重新配置 MCTS 参数用于 Play 模式
77
  play_args = dotdict({
78
+ 'numMCTSSims': 200, # 对战时使用更多的模拟次数
79
  'cpuct': 1.0,
80
  'cuda': args.cuda # 继承 CUDA 设置
81
  })
 
92
 
93
  mcts = MCTS(game, nnet, play_args) #
94
 
95
+ # 【新增】清空历史栈并保存初始状态
96
+ history_stack = []
97
+
98
+ # 获取初始棋盘并保存
99
+ current_board = game.getInitBoard()
100
+ save_state()
101
+
102
 
103
  def get_api_moves(board, player):
104
  """将 getValidMoves 结果从向量转换为 {x, y} 列表"""
 
117
  def check_game_end(board, player):
118
  """检查游戏是否结束,并返回状态信息,基于绝对的棋子数量差异。"""
119
 
 
 
120
  result = game.getGameEnded(board, player) #
121
 
122
  status = 'Ongoing'
123
  score_diff = 0
124
 
125
  if result is not None:
 
 
126
  white_count = np.sum(board == 1)
127
  black_count = np.sum(board == -1)
128
  score_diff = int(white_count - black_count)
129
 
130
+ if result == 0 or score_diff == 0:
131
  status = f"Game Over: Draw. Score: {white_count} vs {black_count}"
132
  elif score_diff > 0:
 
133
  status = f"Game Over: Human (O) Wins! Score: {white_count} vs {black_count}"
134
  elif score_diff < 0:
 
135
  status = f"Game Over: AI (X) Wins! Score: {white_count} vs {black_count}"
136
  else:
 
137
  status = f"Game Over: Draw. Score: {white_count} vs {black_count}"
138
 
139
  return status
140
 
141
  @app.route('/api/game/new', methods=['POST'])
142
  def new_game():
143
+ global current_board, current_player, last_move_coords, board_size, history_stack, first_player_game
144
  data = request.json
145
  size = data.get('size', 8)
146
 
 
147
  first_player = data.get('first_player', 1)
148
+ first_player_game = first_player
149
 
150
+ # 1. 初始化游戏和 AI
151
+ # 只有在尺寸变化时才重新初始化 AI,否则只重置游戏
152
  if game is None or size != board_size:
153
  init_game_and_ai(size)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
+ # 如果尺寸不变,只重置历史栈和棋盘
156
+ history_stack = []
157
+ current_board = game.getInitBoard()
158
+ save_state() # 保存初始状态 (栈长度 = 1)
159
 
160
+ current_player = first_player
161
+ last_move_coords = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
 
163
+ # 2. 处理 AI 先手逻辑
164
+ if current_player == -1:
165
+ # 【修复】删除 history_stack.pop(),因为初始状态 S_init 必须保留。
166
+ # S_init 已经是 current_player = 1 的状态,我们只需要立即触发 AI 移动。
167
+ # AI 移动逻辑 (ai_move_logic) 会执行 AI 动作并保存 S_AI_1 状态。
168
+ history_stack = []
169
 
170
+ # 立即触发 AI 移动
171
+ current_board = np.flip(current_board, 0)
172
+ status = check_game_end(current_board, current_player)
173
+ if status == 'Ongoing':
174
+ # is_init_move=True 确保 AI 逻辑中不再重复 save_state()
175
+ # 因为 S_init 已经被保存,AI 下完后保存 S_AI_1
176
+ return ai_move_logic(is_init_move=False) # 【修正】这里应该是 False,让 ai_move_logic 保存 S_AI_1
177
+
178
+ # 3. 如果是 Human 先手或 AI 先手但游戏结束,则返回当前状态 (S_init)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  status = check_game_end(current_board, current_player)
180
 
 
181
  current_board = np.flip(current_board, 0)
182
 
183
+ # 确保返回给前端的棋盘是垂直翻转的,以匹配前端的坐标系
184
  return jsonify({
185
  'board': current_board.tolist(),
186
  'legal_moves': get_api_moves(current_board, current_player),
187
  'current_player': current_player,
188
  'last_move': last_move_coords,
189
  'status': status,
190
+ 'history_length': len(history_stack) # 返回历史记录长度
191
  })
192
 
 
 
193
 
194
  @app.route('/api/game/human_move', methods=['POST'])
195
  def human_move():
196
+ """处理人类玩家移动,并保存状态,返回给 AI 的中间状态"""
197
  global current_board, current_player, last_move_coords
198
 
199
  if current_player != 1 or check_game_end(current_board, current_player) != 'Ongoing':
200
+ return jsonify({'error': 'Not your turn or game is over', 'history_length': len(history_stack)}), 400
201
 
202
  data = request.json
203
  x = data.get('x')
 
208
  if data.get('action') == 'pass':
209
  action = game.n * game.n # Pass action is the last index
210
  else:
211
+ return jsonify({'error': 'Invalid move coordinates', 'history_length': len(history_stack)}), 400
212
  else:
213
  action = game.n * x + y
214
 
215
  valids = game.getValidMoves(current_board, 1)
216
  if valids[action] == 0:
217
+ return jsonify({'error': 'Illegal move', 'history_length': len(history_stack)}), 400
218
 
219
+ # 1. 执行人���移动
220
  current_board, current_player = game.getNextState(current_board, 1, action)
221
 
222
+ # 2. 【核心修改】保存人类移动后的状态 (State S_H: 轮到 AI 移动)
223
+ save_state()
224
+
225
  if action != game.n * game.n:
226
  last_move_coords = {'x': x, 'y': y}
227
+ else:
228
+ last_move_coords = None # Human Pass
229
 
230
  status = check_game_end(current_board, current_player)
231
 
 
232
  # current_board = np.flip(current_board, 0)
233
 
234
+ # 确保返回给前端的棋盘是垂直翻转的,以匹配前端的坐标系
235
  return jsonify({
236
+ 'board': current_board.tolist(),
237
  'legal_moves': get_api_moves(current_board, current_player),
238
  'current_player': current_player,
239
  'last_move': last_move_coords,
240
  'status': status,
241
+ 'history_length': len(history_stack) # 【新增】返回历史记录长度
242
  })
243
 
244
 
245
+ def ai_move_logic(is_init_move=False):
246
+ """AI 移动的逻辑封装,在 new_game 中调用"""
 
 
 
 
247
  global current_board, current_player, last_move_coords
248
 
249
+ canonical_board = game.getCanonicalForm(current_board, -1) #
250
+
251
+ # 获取 AI 的最佳动作 (temp=0)
252
+ ai_action = np.argmax(mcts.getActionProb(canonical_board, temp=0)) #
253
+
254
+ # 更新游戏状态
255
+ current_board, next_player = game.getNextState(current_board, -1, ai_action) #
 
 
256
  current_player = next_player
257
 
258
  # 记录 AI 的移动坐标
259
+ if ai_action != game.n * game.n: # 如果不是 Pass 动作
260
  ai_x = ai_action // game.n
261
  ai_y = ai_action % game.n
262
  last_move_coords = {'x': int(ai_x), 'y': int(ai_y)}
263
  else:
264
  last_move_coords = None # AI Pass
265
+
266
  status = check_game_end(current_board, current_player)
267
 
268
+ # 【核心修改】保存 AI 移动后的状态 (State S_A: 轮到 Human 移动)
269
+ # is_init_move 标记不再用于控制 save_state,因为 new_game 中只需要 AI 正常执行并保存状态
270
+ save_state()
271
+
272
+ # 确保返回给前端的棋盘是垂直翻转的,以匹配前端的坐标系
273
+ return jsonify({
274
+ 'board': current_board.tolist(),
275
+ 'legal_moves': get_api_moves(current_board, current_player),
276
+ 'current_player': current_player,
277
+ 'last_move': last_move_coords,
278
+ 'status': status,
279
+ 'history_length': len(history_stack) # 【新增】返回历史记录长度
280
+ })
281
+
282
+ # B. 新增 `ai_move` 路由
283
+
284
+ @app.route('/api/game/ai_move', methods=['POST'])
285
+ def ai_move():
286
+ start_time = time.time()
287
+ """触发 AI 移动,并返回最终状态"""
288
+ global current_board, current_player, last_move_coords
289
+
290
+ if current_player != -1:
291
+ return jsonify({'error': 'Not AI turn', 'history_length': len(history_stack)}), 400
292
+
293
+ response = ai_move_logic(is_init_move=False)
294
+
295
  # 控制 AI 最少思考时间为 0.5 秒
296
  end_time = time.time()
297
  used_time = end_time - start_time
298
  if used_time < 0.5:
299
  time.sleep(0.5 - used_time) # 确保至少等待0.5秒
300
 
301
+ return response
302
+
303
+
304
+ # app.py (新增路由)
305
+
306
+ @app.route('/api/game/undo_move', methods=['POST'])
307
+ def undo_move():
308
+ """执行悔棋操作:回退到历史栈中的前一个人类落子完成前的状态(即撤销 Human move + AI move)。"""
309
+ global current_board, current_player, last_move_coords, history_stack
310
+
311
+ # 栈长度至少需要为 3 才能安全地回退一个完整的 (Human + AI) 步骤
312
+ # 3 = S_init + S_Human_move + S_AI_move
313
+ if len(history_stack) < 2:
314
+ # 【修正】如果只有 S_init (长度=1) 或 S_AI_1 (长度=2, 发生在AI先手的第一步),则不能再悔棋了
315
+ return jsonify({
316
+ 'error': 'Cannot undo further. Only initial state remains or insufficient moves made.',
317
+ 'history_length': len(history_stack)
318
+ }), 400
319
+
320
+ # 场景 1: S_init -> S_AI_1 (AI 先手的第一步,长度为 2)
321
+ # 此时只需要 pop() 一次,回到 S_init
322
+ if len(history_stack) == 2:
323
+ # 弹出 S_AI_1 状态
324
+ history_stack.pop()
325
+
326
+ # 场景 2: S_init -> S_H1 -> S_AI_1 (长度 >= 3)
327
+ # 此时需要 pop() 两次,回到 S_H1 之前,即 S_init 或 S_AI_last
328
+ elif len(history_stack) >= 3:
329
+ # 1. 弹出 S_AI 状态 (AI move done, Human turn)
330
+ history_stack.pop()
331
+
332
+ # 2. 弹出 S_Human 状态 (Human move done, AI turn)
333
+ history_stack.pop()
334
+
335
+
336
+ # 3. 恢复到栈顶状态
337
+ current_board_restored, current_player_restored = history_stack[-1]
338
+
339
+ if len(history_stack) == 1 and first_player_game == 1:
340
+ current_board_restored = np.flip(current_board_restored, 0)
341
+
342
+ # 恢复状态
343
+ current_board = np.copy(current_board_restored)
344
+ current_player = current_player_restored
345
+
346
+ # 重置 last_move
347
+ last_move_coords = None
348
+
349
+ status = check_game_end(current_board, current_player)
350
+
351
+ # 确保返回给前端的棋盘是垂直翻转的,以匹配前端的坐标系
352
  return jsonify({
353
  'board': current_board.tolist(),
354
  'legal_moves': get_api_moves(current_board, current_player),
355
  'current_player': current_player,
356
  'last_move': last_move_coords,
357
  'status': status,
358
+ 'history_length': len(history_stack) # 【新增】返回新的历史记录长度
359
  })
360
 
361
  if __name__ == '__main__':
 
365
 
366
  port = int(os.environ.get('PORT', 7860))
367
  # ... (日志) ...
368
+ app.run(host='0.0.0.0', port=port)