我正在参加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代表继续比赛。
python
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
:记录当前最优解的上下界
python
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
时,说明当前分支已经没有继续探索的价值。代码中对应的关键片段:
python
if beta <= alpha:
break # 剪枝!
4.3 实际案例演示
假设当前棋盘:AI(O方)在计算时,发现某步棋可能导致玩家下一步形成双杀。这时Alpha-Beta机制会立即终止该分支的深入计算。
python
X | | O
---------
| X |
---------
O | |
当代码中的check_winner()
函数扫描棋盘时,我总会想起她突然亮起来的眼睛------那年午后,她用手指划过三个连成一线的"O",欢呼声惊飞了窗台的白鸽。如今的AI虽能瞬间判断胜负,却永远读不懂获胜时人类眼底的星光。
在minimax()
函数的递归深处,算法在虚拟时空中推演着万千可能。这多像十七岁那个雨季,我在数学课上草稿本写满的"如果":"如果那天帮她捡起橡皮时多说一句话"、"如果校运会时报名双人项目"...那些未说出口的选择枝桠,最终都成了记忆里的alpha-beta剪枝。
致青春
当我们用算法复现青春的游戏,那些冰冷的代码竟也有了温度。屏幕上的AI永远理性,但当年故意输掉棋局时,藏在规则漏洞里的温柔,才是人类最珍贵的算法。或许人工智能永远学不会,为什么某个初夏的课间,有个少年明明看到必胜棋路,却悄悄把棋子落在了别处。
完整代码示例
python
import sys
import torch
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QGridLayout, QMessageBox
PLAYER = 1
AI = -1
EMPTY = 0
symbols = {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 0
def 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_score
def 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_move
class 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_())