我正在参加Trae「超级体验官」创意实践征文,本文所使用的 Trae 免费下载链接:www.trae.com.cn/?utm_source…
藏在井字棋里的青春密码
十五岁的午后,教室窗外的梧桐叶在初夏的风里沙沙作响。她用铅笔在草稿纸上画下歪歪扭扭的井字格,抬头时马尾辫扫过我的课本。"该你了",她指着第三行第二列的空格,眼睫在阳光下扑闪。那时我们总以为,这九宫格里的"X"与"O"不过是课间十分钟的游戏,却不知那些交错的三连棋里,藏着后来让我痴迷的算法奥秘。
二十年后的今天,当我用代码复现这个游戏时,屏幕上的AI总能精准落子。那些被橡皮擦抹去的棋局,那些故意输掉的对局,那些藏在胜负里的小心思,忽然在记忆里鲜活起来——原来当年我们稚嫩的博弈策略,早已预言了人工智能领域最经典的Minimax算法。
Trae 圆梦
首先,我们向Trae发出请求: “基于pytorch或者TensorFlow实现井字棋AI开发(Minimax算法+alpha-beta剪枝)”
随后系统会自动生成大量代码,其中部分内容你可能熟悉,也可能有些陌生。不过无需担心,我们可以根据提示信息,将这些代码逐一复制粘贴到对应的项目文件中。
完成代码的复制粘贴后,请运行 main.py
文件。若程序正常运行,控制台将进入交互模式,此时可以开始输入指令。
由于命令行交互体验不佳,我们进一步向 Trae 发送指令: “使用 PyQt5 实现 GUI 界面” ,以优化用户操作体验。
执行效果如下:
现在,这个小游戏已经可以发送给你的初恋对象啦!下面我们简单分析下游戏原理:为什么当年玩井字棋总是赢不了呢?
藏在青春悸动里的决策智慧
2.1 青涩的"如果体"思维
"如果我把棋子放在这里,她会怎么应对?"当年的课桌上,我们本能地推演着两三步后的棋局。这种朴素的预判思维,正是Minimax算法的雏形。
就像少年在纸条上写下又划去的告白词,算法也在虚拟的决策树上反复权衡:
- 若此刻落子角落(最大收益)但对方可能封堵中间(最小收益)于是转而抢占中心(平衡策略)
2.2 懵懂的最大最小原则
那时的我们不懂算法,却在本能践行着核心逻辑:
- 自己回合(最大化层):偷偷创造双杀机会对方回合(极小化层):假设她会做出最聪明的应对最终选择:在所有"最坏可能"里选"相对最好"
这种思维模式,像极了初恋时小心翼翼的试探:既期待对方察觉心意,又害怕被直接拒绝后的尴尬。算法中的评分机制,恰似少年藏在课桌抽屉里的日记,给每个选择默默打分。
逐行解析核心算法
3.1 胜负判定函数
这个函数就像赛场裁判,每走一步就检查是否有玩家连成直线。返回1表示玩家胜,-1表示AI胜,0代表继续比赛。
def check_winner(board): # 检查横向三连 for i in range(3): if board[i,0] !=0 and (board[i] == board[i,0]).all(): return board[i,0] # 检查纵向三连(类似逻辑) # 检查两条对角线 return 0
3.2 Minimax递归框架
这个递归函数像一位深谋远虑的军师,不断推演后续可能的发展。参数说明:
depth
:记录递归深度(井字棋最多9层)is_maximizing
:当前是AI还是玩家回合alpha
/beta
:记录当前最优解的上下界def minimax(board, depth, is_maximizing, alpha, beta): # 终止条件 winner = check_winner(board) if winner !=0: return 1 if winner==-1 else -1 # AI胜得1分 if 棋盘已满: return 0 # 平局 if is_maximizing: # AI回合 best = -∞ for 每个空位: 模拟落子 score = minimax(新棋盘, depth+1, False, alpha, beta) best = max(best, score) alpha = max(alpha, best) if beta <= alpha: # Alpha-Beta剪枝 break return best else: # 玩家回合 # 类似逻辑,取最小值
Alpha-Beta剪枝原理
4.1 剪枝的必要性
假设AI在评估某个走法时,发现后续存在必输的局面,就可以立即放弃这个分支。就像下棋时说:"这步棋走下去肯定要输,不用再算了!"
4.2 剪枝的实现技巧
通过维护两个值:
alpha
:已知的玩家最少能给AI的分数beta
:已知的AI最多能获得的分数当alpha >= beta
时,说明当前分支已经没有继续探索的价值。代码中对应的关键片段:
if beta <= alpha: break # 剪枝!
4.3 实际案例演示
假设当前棋盘:AI(O方)在计算时,发现某步棋可能导致玩家下一步形成双杀。这时Alpha-Beta机制会立即终止该分支的深入计算。
X | | O--------- | X | ---------O | |
当代码中的check_winner()
函数扫描棋盘时,我总会想起她突然亮起来的眼睛——那年午后,她用手指划过三个连成一线的"O",欢呼声惊飞了窗台的白鸽。如今的AI虽能瞬间判断胜负,却永远读不懂获胜时人类眼底的星光。
在minimax()
函数的递归深处,算法在虚拟时空中推演着万千可能。这多像十七岁那个雨季,我在数学课上草稿本写满的"如果":"如果那天帮她捡起橡皮时多说一句话"、"如果校运会时报名双人项目"...那些未说出口的选择枝桠,最终都成了记忆里的alpha-beta剪枝。
致青春
当我们用算法复现青春的游戏,那些冰冷的代码竟也有了温度。屏幕上的AI永远理性,但当年故意输掉棋局时,藏在规则漏洞里的温柔,才是人类最珍贵的算法。或许人工智能永远学不会,为什么某个初夏的课间,有个少年明明看到必胜棋路,却悄悄把棋子落在了别处。
完整代码示例
import sysimport torchfrom PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QGridLayout, QMessageBoxPLAYER = 1AI = -1EMPTY = 0symbols = {PLAYER: 'X', AI: 'O', EMPTY: ' '}def check_winner(board): for i in range(3): if board[i, 0] != 0 and (board[i] == board[i, 0]).all(): return board[i, 0].item() if board[0, i] != 0 and (board[:, i] == board[0, i]).all(): return board[0, i].item() if board[1, 1] != 0: if (board[0, 0] == board[1, 1] and board[2, 2] == board[1, 1]) or \ (board[0, 2] == board[1, 1] and board[2, 0] == board[1, 1]): return board[1, 1].item() return 0def minimax(board, depth, is_maximizing, alpha, beta): winner = check_winner(board) if winner != 0: return 10 - depth if winner == AI else depth - 10 if torch.all(board != 0): return 0 if is_maximizing: best_score = -float('inf') for move in torch.nonzero(board == 0): row, col = move new_board = board.clone() new_board[row, col] = AI score = minimax(new_board, depth + 1, False, alpha, beta) best_score = max(best_score, score) alpha = max(alpha, best_score) if beta <= alpha: break return best_score else: best_score = float('inf') for move in torch.nonzero(board == 0): row, col = move new_board = board.clone() new_board[row, col] = PLAYER score = minimax(new_board, depth + 1, True, alpha, beta) best_score = min(best_score, score) beta = min(beta, best_score) if beta <= alpha: break return best_scoredef find_best_move(board): best_score = -float('inf') best_move = None for move in torch.nonzero(board == 0): row, col = move new_board = board.clone() new_board[row, col] = AI score = minimax(new_board, 0, False, -float('inf'), float('inf')) if score > best_score: best_score = score best_move = (row.item(), col.item()) return best_moveclass TicTacToe(QWidget): def __init__(self): super().__init__() self.setWindowTitle("Tic Tac Toe - PyQt5") self.board = torch.zeros((3, 3), dtype=torch.int) self.buttons = [[None for _ in range(3)] for _ in range(3)] self.init_ui() def init_ui(self): layout = QGridLayout() for row in range(3): for col in range(3): btn = QPushButton('') btn.setFixedSize(100, 100) btn.setStyleSheet("font-size: 24px;") btn.clicked.connect(lambda _, r=row, c=col: self.player_move(r, c)) self.buttons[row][col] = btn layout.addWidget(btn, row, col) self.setLayout(layout) def player_move(self, row, col): if self.board[row, col] != 0: return self.board[row, col] = PLAYER self.update_ui() if self.check_game_end(): return self.ai_move() def ai_move(self): move = find_best_move(self.board) if move: row, col = move self.board[row, col] = AI self.update_ui() self.check_game_end() def update_ui(self): for row in range(3): for col in range(3): self.buttons[row][col].setText(symbols[self.board[row, col].item()]) def check_game_end(self): winner = check_winner(self.board) if winner != 0: QMessageBox.information(self, "Game Over", "You win!" if winner == PLAYER else "AI wins!") self.reset_game() return True elif torch.all(self.board != 0): QMessageBox.information(self, "Game Over", "It's a tie!") self.reset_game() return True return False def reset_game(self): self.board[:] = 0 self.update_ui()if __name__ == '__main__': app = QApplication(sys.argv) window = TicTacToe() window.show() sys.exit(app.exec_())