ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

黑白棋强化学习毕业设计:从Q-Learning到DQN的完整实战

黑白棋强化学习毕业设计:从Q-Learning到DQN的完整实战 简介这份资源是面向高校计算机相关专业学生与强化学习入门者的毕业设计完整项目包围绕Python实现的黑白棋翻转棋游戏展开重点解决如何用强化学习训练具备自我进化能力的棋类AI这一实践课题。包内共67个文件以21个py源码、8个png与5个jpg界面素材、7个pdf文档为主另含svg、psd、md、h5、html、js、css、yml、tex等辅助文件压缩包约64.72MB涵盖游戏逻辑、界面渲染、模型训练与评估等模块。项目构建了Q-Learning、DQN、策略梯度等强化学习模型并设计经验回放、探索与利用平衡等训练流程配合Pygame界面支持人机对战与AI自我对战。已有160人学习下载适合作为毕业设计参考或强化学习实战练手读者可据此理解棋类AI从环境搭建到策略优化的完整链路并借鉴其目录组织与训练评估思路。1. 黑白棋 强化学习一个毕业设计为什么值得认真做很多计算机毕业设计选题做出来就是「能跑就行」——一个带界面的黑白棋玩家点一下、AI 走一步论文里写两句极大极小搜索答辩时演示一遍结束。但如果你把 AI 那部分换成强化学习整个项目的含金量会完全不一样。黑白棋Reversi / Othello规则简单、状态空间适中、回合制、零和博弈天然适合作为强化学习算法的实验床。它不像围棋那样需要天量算力也不像井字棋那样简单到没有区分度刚好卡在一个「用普通笔记本就能训练、又能体现算法差异」的甜点区。这篇笔记面向正在做计算机毕业设计、想用 Python 把黑白棋和强化学习结合起来的人。我会从环境搭建讲到算法选型、训练调参、GUI 集成再到论文里怎么写实验对比。核心思路是用 Python 实现一个标准的黑白棋对弈环境然后分别用 Q-Learning 和 DQN 两种方式训练 AI 棋手最后通过胜率曲线和棋力评估来验证效果。整套方案不需要 GPU 集群一台普通笔记本就能跑通。如果你正在纠结毕业设计选题或者已经选了但不知道怎么把强化学习真正落地下面的内容可以直接抄作业。2. 环境搭建与黑白棋规则引擎从零写出可训练的对弈环境2.1 Python 环境与依赖选型做强化学习项目环境配置是第一步也是最容易翻车的地方。我一般推荐用 Python 3.9 或 3.10太新的版本3.12有些强化学习库还没跟上太老的3.7 以下又缺少一些语法特性。安装方式上直接用 python.org 下载安装包最省心安装时记得勾选「Add Python to PATH」不然后面在命令行里调 python 会报「不是内部或外部命令」。如果你用 VS Code 做开发装好 Python 扩展后按 CtrlShiftP 输入「Python: Select Interpreter」选择你安装的解释器就行。虚拟环境建议用 venv别用 conda因为 conda 装 PyTorch 有时候会引入一些奇怪的依赖冲突。# 创建虚拟环境 python -m venv othello_rl # 激活Windows othello_rl\Scripts\activate # 激活macOS/Linux source othello_rl/bin/activate # 安装核心依赖 pip install numpy matplotlib pygame torch这里解释一下每个依赖的作用numpy 负责棋盘状态的矩阵运算matplotlib 用来画训练曲线论文里要用pygame 做可视化界面torch 提供 DQN 所需的神经网络和自动求导。版本方面不用刻意锁死pip 默认装最新稳定版就行但如果 torch 安装超过 2GB 说明下成了 GPU 版本普通笔记本用 CPU 版就够可以用pip install torch --index-url https://download.pytorch.org/whl/cpu指定 CPU 版本。提示如果你在校园网环境下 pip 下载慢可以临时换用清华源-i https://pypi.tuna.tsinghua.edu.cn/simple但不要长期全局配置避免版本滞后。2.2 黑白棋规则引擎的实现黑白棋的规则本身不复杂但有几个边界情况必须处理对否则训练出来的 AI 会学到错误的策略。核心规则是黑棋先行落子必须夹住对方至少一个棋子被夹住的棋子翻转成己方颜色双方都无子可落时游戏结束棋子多者胜。下面是一个精简但完整的规则引擎实现import numpy as np # 8个方向向量上、下、左、右、左上、右上、左下、右下 DIRECTIONS [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)] class OthelloEnv: def __init__(self): # 0空, 1黑棋, -1白棋 self.board np.zeros((8, 8), dtypeint) # 初始布局中心四格 self.board[3, 3] -1 self.board[4, 4] -1 self.board[3, 4] 1 self.board[4, 3] 1 self.current_player 1 # 黑棋先行 def get_valid_moves(self, player): 返回当前玩家所有合法落子位置 moves [] for r in range(8): for c in range(8): if self.board[r, c] ! 0: continue if self._would_flip(r, c, player): moves.append((r, c)) return moves def _would_flip(self, row, col, player): 判断在(row,col)落子是否能翻转对方棋子 for dr, dc in DIRECTIONS: r, c row dr, col dc found_opponent False while 0 r 8 and 0 c 8: if self.board[r, c] -player: found_opponent True elif self.board[r, c] player: if found_opponent: return True break else: break r dr c dc return False def step(self, row, col): 执行落子翻转棋子切换玩家 if (row, col) not in self.get_valid_moves(self.current_player): raise ValueError(f非法落子: ({row},{col})) self.board[row, col] self.current_player # 翻转所有被夹住的棋子 for dr, dc in DIRECTIONS: r, c row dr, col dc to_flip [] while 0 r 8 and 0 c 8: if self.board[r, c] -self.current_player: to_flip.append((r, c)) elif self.board[r, c] self.current_player: for fr, fc in to_flip: self.board[fr, fc] self.current_player break else: break r dr c dc self.current_player -self.current_player def is_terminal(self): 判断游戏是否结束 return (len(self.get_valid_moves(1)) 0 and len(self.get_valid_moves(-1)) 0) def get_winner(self): 返回胜者1黑胜-1白胜0平局 black np.sum(self.board 1) white np.sum(self.board -1) if black white: return 1 elif white black: return -1 return 0这段代码的关键点在于_would_flip和step里的翻转逻辑。_would_flip沿八个方向扫描只有「对方棋子连续出现、且末端是己方棋子」才算合法。step里翻转时要注意先把待翻转的坐标收集到列表里确认方向末端是己方棋子后再统一翻转不能边扫描边翻否则会破坏后续方向的判断。参数方面棋盘固定 8×8初始布局固定中心四格这些是黑白棋标准规则不需要改。如果你想让 AI 训练更快可以把棋盘缩小到 6×6 做快速验证但论文里最终实验必须用 8×8 标准棋盘。2.3 状态编码与动作空间设计强化学习需要一个数值化的状态表示。最直接的方式是把 8×8 棋盘展平成 64 维向量每个位置取值 {0, 1, -1}。但这样有个问题从当前玩家的视角看己方棋子和对方棋子应该有不同的语义。所以我一般会把状态编码成两个 64 维的二进制向量一个表示己方棋子位置一个表示对方棋子位置拼起来就是 128 维。def encode_state(board, player): 将棋盘编码为128维向量己方视角 my_pieces (board player).astype(float).flatten() opp_pieces (board -player).astype(float).flatten() return np.concatenate([my_pieces, opp_pieces]) def decode_action(action): 将0-63的动作编号转为(row, col) return action // 8, action % 8 def encode_action(row, col): 将(row, col)转为0-63的动作编号 return row * 8 col动作空间就是 64 个格子但并非所有格子都合法。训练时有两种处理方式一是把所有非法动作的 Q 值设为负无穷选动作时只在合法动作里挑二是用动作掩码action mask在神经网络输出后把非法位置屏蔽掉。第一种简单直接适合 Q-Learning第二种是 DQN 的标准做法后面会详细讲。注意状态编码一定要从「当前玩家视角」出发否则同一个棋盘局面在黑白双方看来编码不同网络学起来会很混乱。这是很多人第一次做博弈类强化学习时踩的坑。3. Q-Learning 实现用查表法先跑通一个能赢随机棋手的 AI3.1 Q-Learning 在黑白棋上的适配Q-Learning 是最经典的强化学习算法核心就一个公式Q(s,a) ← Q(s,a) α[r γ·max Q(s,a) - Q(s,a)]。它维护一张 Q 表记录每个状态-动作对的价值。在黑白棋里状态是棋盘编码动作是 64 个格子之一。但这里有个现实问题黑白棋的状态空间虽然比围棋小得多但仍然有大约 10^28 种合法局面用查表法根本存不下。所以实际做法是用 Q-Learning 跑一个小规模版本比如 4×4 或 6×6 棋盘或者只对前几步做查表后面用随机策略补齐。这样做的目的是先验证规则引擎和训练流程没问题再上 DQN。我一般会先用 6×6 棋盘跑 Q-Learning状态用字典存储键是棋盘编码的哈希值。这样大概能存几十万个状态普通内存扛得住。import random from collections import defaultdict class QLearningAgent: def __init__(self, alpha0.1, gamma0.95, epsilon1.0): self.q_table defaultdict(lambda: np.zeros(64)) self.alpha alpha # 学习率 self.gamma gamma # 折扣因子 self.epsilon epsilon # 探索率 def choose_action(self, state, valid_moves): epsilon-greedy策略选择动作 if random.random() self.epsilon: return random.choice(valid_moves) q_values self.q_table[state] # 只在合法动作中选Q值最大的 best_action max(valid_moves, keylambda a: q_values[a]) return best_action def update(self, state, action, reward, next_state, next_valid_moves, done): Q-Learning更新公式 current_q self.q_table[state][action] if done: target reward else: next_q self.q_table[next_state] max_next_q max(next_q[a] for a in next_valid_moves) if next_valid_moves else 0 target reward self.gamma * max_next_q self.q_table[state][action] self.alpha * (target - current_q)参数说明alpha0.1是学习率太大容易震荡太小收敛慢gamma0.95是折扣因子黑白棋一局大概 60 步0.95 的衰减在 60 步后约为 0.046能较好地平衡当前和未来收益epsilon从 1.0 开始每训练一局衰减 0.995最低降到 0.05保证后期仍有少量探索。3.2 训练循环与奖励设计奖励设计直接决定 AI 学到的策略。最简单的做法是终局奖励赢了 1输了 -1平局 0。但黑白棋有个特点——中盘领先不代表最终赢因为最后几步可能被大量翻转。所以纯终局奖励会让训练很慢。我一般会加一个中间奖励每步落子后己方棋子数与对方棋子数的差值乘以一个小系数比如 0.01鼓励 AI 在中盘也保持棋子优势。但系数不能太大否则 AI 会变得短视只顾眼前翻棋子不顾位置质量。def train_q_learning(episodes50000): agent QLearningAgent() win_history [] for ep in range(episodes): env OthelloEnv() state encode_state(env.board, env.current_player) total_reward 0 while not env.is_terminal(): valid_moves env.get_valid_moves(env.current_player) if not valid_moves: env.current_player -env.current_player continue action agent.choose_action(state, [encode_action(r, c) for r, c in valid_moves]) row, col decode_action(action) env.step(row, col) next_state encode_state(env.board, env.current_player) next_valid env.get_valid_moves(env.current_player) next_valid_encoded [encode_action(r, c) for r, c in next_valid] # 中间奖励棋子差值的微小系数 piece_diff np.sum(env.board -env.current_player) - np.sum(env.board env.current_player) reward 0.01 * piece_diff done env.is_terminal() if done: winner env.get_winner() reward 1.0 if winner 1 else (-1.0 if winner -1 else 0.0) agent.update(state, action, reward, next_state, next_valid_encoded, done) state next_state # epsilon衰减 agent.epsilon max(0.05, agent.epsilon * 0.995) win_history.append(1 if env.get_winner() 1 else 0) if (ep 1) % 5000 0: recent_win_rate np.mean(win_history[-5000:]) print(fEpisode {ep1}, 近5000局胜率: {recent_win_rate:.3f}, epsilon: {agent.epsilon:.3f}) return agent这段训练循环里有个细节当某一方无子可落时直接切换玩家不执行step。这是黑白棋规则的一部分很多人第一次写会漏掉导致 AI 在无子可落时卡死。训练 50000 局大概需要 10-20 分钟取决于机器性能。如果胜率曲线在 0.7 以上并且稳定说明 Q-Learning 已经能稳定击败随机棋手。但注意这里的「胜率」是对随机对手的不是对最优策略的。论文里如果要体现算法优势需要让 Q-Learning 和 DQN 互相对战或者和固定深度的极大极小搜索对战。3.3 训练结果的可视化与评估训练完之后用 matplotlib 画出胜率曲线和 epsilon 衰减曲线这两张图在论文里是必须的。import matplotlib.pyplot as plt def plot_training(win_history, window500): 绘制滑动平均胜率曲线 smoothed np.convolve(win_history, np.ones(window)/window, modevalid) plt.figure(figsize(10, 5)) plt.plot(smoothed, labelfWin Rate (MA-{window})) plt.xlabel(Episode) plt.ylabel(Win Rate vs Random) plt.title(Q-Learning Training Curve on Othello) plt.legend() plt.grid(True) plt.savefig(q_learning_curve.png, dpi150) plt.show()评估时不能只看训练胜率还要做独立测试固定训练好的 Q 表让 AI 和随机棋手对战 1000 局统计胜率。如果测试胜率和训练后期胜率差距超过 10%说明过拟合了需要增大 epsilon 的最小值或者减少训练局数。提示Q-Learning 在 8×8 棋盘上查表法基本不可行状态太多。我一般用 6×6 棋盘做 Q-Learning 验证8×8 棋盘直接上 DQN。论文里可以写「Q-Learning 在小规模棋盘上验证了算法可行性DQN 在标准棋盘上实现了更强棋力」这样逻辑是通的。4. DQN 实现用神经网络替代 Q 表在 8×8 标准棋盘上训练4.1 网络结构与动作掩码DQN 的核心是用一个神经网络近似 Q(s,a)。输入是 128 维状态向量输出是 64 维 Q 值向量每个分量对应一个格子的价值。网络结构不用太深两层全连接256 隐藏单元 ReLU 就够了太深反而容易过拟合而且训练慢。import torch import torch.nn as nn import torch.optim as optim class QNetwork(nn.Module): def __init__(self): super().__init__() self.fc nn.Sequential( nn.Linear(128, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU(), nn.Linear(256, 64) ) def forward(self, x): return self.fc(x)动作掩码是 DQN 在博弈类任务里的关键技巧。因为 64 个格子里通常只有几个是合法的如果不加掩码网络会花大量精力去学「哪些格子不能下」效率很低。做法是在网络输出后把非法动作的 Q 值设为一个很大的负数比如 -1e9这样 argmax 时自然不会选到。def select_action(network, state, valid_actions, epsilon): 带动作掩码的epsilon-greedy if random.random() epsilon: return random.choice(valid_actions) with torch.no_grad(): q_values network(torch.FloatTensor(state).unsqueeze(0)).squeeze() # 掩码非法动作设为负无穷 mask torch.full((64,), -1e9) for a in valid_actions: mask[a] 0 q_values q_values mask return q_values.argmax().item()这里mask初始全为 -1e9合法动作位置设为 0加到 Q 值上后非法动作就变成极小数。注意不要用float(-inf)因为和 0 相加可能产生 nan用 -1e9 足够小且安全。4.2 经验回放与目标网络DQN 相比 Q-Learning 多了两个关键组件经验回放池Replay Buffer和目标网络Target Network。经验回放池存最近的 (s, a, r, s, done) 五元组训练时随机采样打破时间相关性。目标网络是主网络的副本每隔 C 步同步一次参数用来计算 TD 目标避免训练震荡。from collections import deque class ReplayBuffer: def __init__(self, capacity50000): self.buffer deque(maxlencapacity) def push(self, state, action, reward, next_state, done, next_valid): self.buffer.append((state, action, reward, next_state, done, next_valid)) def sample(self, batch_size): batch random.sample(self.buffer, batch_size) states, actions, rewards, next_states, dones, next_valids zip(*batch) return (np.array(states), np.array(actions), np.array(rewards), np.array(next_states), np.array(dones), next_valids) def __len__(self): return len(self.buffer)目标网络同步的代码def update_target_network(main_net, target_net): target_net.load_state_dict(main_net.state_dict())训练主循环里每 C500 步调用一次update_target_network。经验回放池容量设 50000batch_size 设 64。这些参数不是绝对的容量太小容易遗忘早期经验太大则训练慢batch_size 太小梯度噪声大太大显存吃紧。64 是一个比较稳的默认值。4.3 完整训练流程与超参数设置把上面这些组件串起来DQN 的训练流程如下def train_dqn(episodes20000, batch_size64, gamma0.99, lr1e-3, epsilon_start1.0, epsilon_end0.05, epsilon_decay0.9995, target_update500): main_net QNetwork() target_net QNetwork() target_net.load_state_dict(main_net.state_dict()) optimizer optim.Adam(main_net.parameters(), lrlr) buffer ReplayBuffer(capacity50000) epsilon epsilon_start step_count 0 win_history [] for ep in range(episodes): env OthelloEnv() state encode_state(env.board, env.current_player) while not env.is_terminal(): valid_moves env.get_valid_moves(env.current_player) if not valid_moves: env.current_player -env.current_player continue valid_encoded [encode_action(r, c) for r, c in valid_moves] action select_action(main_net, state, valid_encoded, epsilon) row, col decode_action(action) env.step(row, col) next_state encode_state(env.board, env.current_player) next_valid env.get_valid_moves(env.current_player) next_valid_encoded [encode_action(r, c) for r, c in next_valid] done env.is_terminal() if done: winner env.get_winner() reward 1.0 if winner 1 else (-1.0 if winner -1 else 0.0) else: piece_diff np.sum(env.board -env.current_player) - np.sum(env.board env.current_player) reward 0.01 * piece_diff buffer.push(state, action, reward, next_state, done, next_valid_encoded) state next_state step_count 1 # 训练网络 if len(buffer) batch_size: states, actions, rewards, next_states, dones, next_valids buffer.sample(batch_size) states_t torch.FloatTensor(states) actions_t torch.LongTensor(actions) rewards_t torch.FloatTensor(rewards) next_states_t torch.FloatTensor(next_states) dones_t torch.FloatTensor(dones) # 当前Q值 q_values main_net(states_t).gather(1, actions_t.unsqueeze(1)).squeeze() # 目标Q值用目标网络 with torch.no_grad(): next_q target_net(next_states_t) # 对每个样本单独做动作掩码 max_next_q [] for i, nv in enumerate(next_valids): if len(nv) 0: max_next_q.append(0.0) else: mask torch.full((64,), -1e9) for a in nv: mask[a] 0 max_next_q.append((next_q[i] mask).max().item()) max_next_q torch.FloatTensor(max_next_q) targets rewards_t gamma * max_next_q * (1 - dones_t) loss nn.MSELoss()(q_values, targets) optimizer.zero_grad() loss.backward() # 梯度裁剪防止梯度爆炸 nn.utils.clip_grad_norm_(main_net.parameters(), 1.0) optimizer.step() # 目标网络同步 if step_count % target_update 0: update_target_network(main_net, target_net) epsilon max(epsilon_end, epsilon * epsilon_decay) win_history.append(1 if env.get_winner() 1 else 0) if (ep 1) % 1000 0: recent np.mean(win_history[-1000:]) print(fEpisode {ep1}, 近1000局胜率: {recent:.3f}, epsilon: {epsilon:.3f}) return main_net, win_history超参数方面gamma0.99比 Q-Learning 的 0.95 更大因为 DQN 用神经网络泛化可以看得更远lr1e-3是 Adam 的常用值epsilon_decay0.9995比 Q-Learning 慢因为 DQN 需要更多样本才能稳定target_update500表示每 500 步同步一次目标网络。梯度裁剪clip_grad_norm_设 1.0这是防止 DQN 训练中梯度爆炸的后悔药不加的话 loss 可能突然变成 nan。训练 20000 局大概需要 2-4 小时CPU如果机器有 GPU 会快很多。训练完成后同样要做独立测试固定网络参数和随机棋手对战 1000 局统计胜率。正常情况下 DQN 对随机棋手的胜率应该在 90% 以上。4.4 模型保存与加载训练好的模型要保存下来方便后续评估和 GUI 集成。# 保存 torch.save(main_net.state_dict(), dqn_othello.pth) # 加载 def load_model(pathdqn_othello.pth): net QNetwork() net.load_state_dict(torch.load(path, map_locationcpu)) net.eval() return netmap_locationcpu是为了兼容在 GPU 上训练、在 CPU 上推理的场景。net.eval()会关闭 Dropout 和 BatchNorm 的训练模式推理时必须调用。5. 避坑与排查训练不收敛、胜率上不去、GUI 卡死怎么办5.1 训练 loss 不降反升现象DQN 训练几百局后loss 从 0.5 左右突然飙升到几百甚至 nan。原因最常见的是梯度爆炸其次是奖励尺度太大。黑白棋终局奖励 ±1 是合理的但如果中间奖励系数设成 0.1 甚至更大累积起来会远超 1导致 Q 值发散。解决第一加梯度裁剪clip_grad_norm_(params, 1.0)第二中间奖励系数降到 0.01 以下第三检查目标网络是否正常同步如果 target_net 一直不更新TD 目标会越来越大。5.2 胜率卡在 50% 上不去现象训练了几万局对随机棋手胜率始终在 50% 左右和瞎下差不多。原因大概率是状态编码有问题。比如没有从当前玩家视角编码导致黑白双方共用一套 Q 值网络学不明白。另一个可能是动作掩码没做对网络选了很多非法动作实际有效训练样本很少。解决检查encode_state是否根据player参数翻转了视角检查select_action里 mask 是否正确屏蔽了非法动作。可以在训练时打印每局的有效步数如果远小于正常局数约 60 步说明掩码有问题。5.3 训练速度越来越慢现象前 1000 局很快后面每局耗时越来越长。原因经验回放池用deque存 numpy 数组每次 sample 都要做np.array转换数据量大了之后内存拷贝开销很大。另外 Q 表如果用 defaultdict 存状态多了之后哈希冲突也会变慢。解决经验回放池改用预分配 numpy 数组 环形指针避免频繁内存分配。Q-Learning 的 Q 表如果太大考虑用近似方法或者限制状态数量。5.4 GUI 界面点击后无响应现象pygame 界面能显示棋盘但点击落子后卡死。原因pygame 的事件循环和 AI 推理在同一个线程里AI 推理尤其是 DQN 前向传播耗时较长时事件循环被阻塞界面就卡死了。解决把 AI 推理放到单独线程里或者用pygame.event.pump()在推理前后手动刷新事件队列。更简单的做法是AI 推理前先显示「思考中」提示推理完再刷新界面。如果推理时间超过 1 秒建议用多线程。import threading def ai_move_async(env, network, callback): 在子线程中执行AI推理 def worker(): valid env.get_valid_moves(env.current_player) valid_encoded [encode_action(r, c) for r, c in valid] state encode_state(env.board, env.current_player) action select_action(network, state, valid_encoded, epsilon0.0) callback(action) t threading.Thread(targetworker) t.start()5.5 论文实验数据不可复现现象答辩时老师让你重新跑一遍实验结果和论文里的曲线对不上。原因没有固定随机种子。Python 的 random、numpy 的 random、torch 的 random 都是独立随机源不固定的话每次训练结果都不一样。解决在训练脚本开头固定所有随机种子。import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)固定种子后每次训练结果完全一致论文里的曲线和表格才能复现。这是毕业设计答辩的基本要求别偷懒。6. 把 AI 接进 pygame 界面从训练脚本到可演示的毕业设计成品训练好的模型如果只停留在命令行里答辩演示效果会差很多。一个能点击、能落子、能显示胜负的图形界面是毕业设计的门面。pygame 足够轻量适合做这种棋盘类游戏。先搭一个最小的可交互棋盘import pygame import sys # 常量 CELL 80 MARGIN 40 WIDTH CELL * 8 MARGIN * 2 HEIGHT CELL * 8 MARGIN * 2 BLACK (0, 0, 0) WHITE (255, 255, 255) GREEN (0, 128, 0) GRAY (200, 200, 200) def draw_board(screen, env, valid_moves): screen.fill(GREEN) # 画网格 for i in range(9): pygame.draw.line(screen, BLACK, (MARGIN, MARGIN i*CELL), (MARGIN 8*CELL, MARGIN i*CELL), 2) pygame.draw.line(screen, BLACK, (MARGIN i*CELL, MARGIN), (MARGIN i*CELL, MARGIN 8*CELL), 2) # 画棋子 for r in range(8): for c in range(8): center (MARGIN c*CELL CELL//2, MARGIN r*CELL CELL//2) if env.board[r, c] 1: pygame.draw.circle(screen, BLACK, center, CELL//2 - 5) elif env.board[r, c] -1: pygame.draw.circle(screen, WHITE, center, CELL//2 - 5) # 标记合法落子位置 for r, c in valid_moves: center (MARGIN c*CELL CELL//2, MARGIN r*CELL CELL//2) pygame.draw.circle(screen, GRAY, center, 8) pygame.display.flip()主循环里处理鼠标点击把像素坐标转成棋盘坐标判断是否合法然后执行落子。AI 走棋时调用训练好的网络选 Q 值最大的合法动作。def main(): pygame.init() screen pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption(Othello - DQN AI) clock pygame.time.Clock() env OthelloEnv() network load_model(dqn_othello.pth) human_player 1 # 人类执黑 running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False elif event.type pygame.MOUSEBUTTONDOWN and env.current_player human_player: x, y event.pos col (x - MARGIN) // CELL row (y - MARGIN) // CELL if 0 row 8 and 0 col 8: if (row, col) in env.get_valid_moves(env.current_player): env.step(row, col) # AI回合 if env.current_player ! human_player and not env.is_terminal(): valid env.get_valid_moves(env.current_player) if valid: state encode_state(env.board, env.current_player) valid_encoded [encode_action(r, c) for r, c in valid] action select_action(network, state, valid_encoded, epsilon0.0) row, col decode_action(action) env.step(row, col) else: env.current_player -env.current_player valid_moves env.get_valid_moves(env.current_player) if not env.is_terminal() else [] draw_board(screen, env, valid_moves) if env.is_terminal(): winner env.get_winner() font pygame.font.SysFont(simhei, 48) text 黑棋胜 if winner 1 else (白棋胜 if winner -1 else 平局) label font.render(text, True, (255, 0, 0)) screen.blit(label, (WIDTH//2 - 80, HEIGHT//2 - 24)) pygame.display.flip() pygame.time.wait(3000) running False clock.tick(30) pygame.quit() sys.exit()这里有几个细节值得注意。第一AI 走棋时epsilon0.0表示完全利用不探索否则演示时会下出随机棋。第二如果 AI 无子可落直接切换玩家不调用step。第三终局显示胜负后等 3 秒再退出方便演示时截图。如果你想让界面更「毕业设计」一点可以加一个侧边栏显示当前轮到谁、双方棋子数、AI 思考时间。这些信息在答辩时能体现工作量。但别过度设计界面花哨不等于技术含量高老师更看重算法部分。最后说一个我自己的习惯训练脚本和 GUI 脚本分开写训练好的模型保存成.pth文件GUI 只负责加载和推理。这样调试 GUI 时不用重新训练改界面也不影响算法。论文里可以把训练和推理分成两个章节写逻辑更清晰。希望帮到你。本文还有配套的精品资源点击获取
返回列表