Spaces:
Sleeping
Sleeping
update app.py
Browse files
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':
|
| 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 |
-
|
| 168 |
-
|
| 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 |
-
#
|
| 189 |
-
|
| 190 |
-
#
|
|
|
|
|
|
|
|
|
|
| 191 |
|
| 192 |
-
#
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
#
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
#
|
| 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 |
-
"""
|
| 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 |
-
#
|
| 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 |
-
|
| 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 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 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)
|