Python实现五子棋AI对战
五子棋是一款经典的两人对战棋类游戏,规则简单但策略丰富。本文将使用Python和Pygame实现一个完整的五子棋游戏,并使用Minimax算法配合Alpha-Beta剪枝开发一个有挑战性的AI对手。
一、五子棋规则与设计
五子棋的规则非常简单:两位玩家轮流在棋盘上落子,谁先在横、竖、斜任一方向上连成5个同色棋子即获胜。我们将实现一个15x15的标准棋盘。
# 游戏常量定义
BOARD_SIZE = 15 # 棋盘大小 15x15
CELL_SIZE = 40 # 每格像素大小
MARGIN = 30 # 棋盘边距
WINDOW_WIDTH = BOARD_SIZE * CELL_SIZE + MARGIN * 2
WINDOW_HEIGHT = BOARD_SIZE * CELL_SIZE + MARGIN * 2 + 60 # 底部状态栏
# 颜色定义
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
BOARD_COLOR = (220, 179, 92) # 棋盘木色
LINE_COLOR = (80, 50, 20) # 网格线颜色
BG_COLOR = (240, 230, 200) # 背景色
HIGHLIGHT_COLOR = (255, 0, 0) # 高亮(最后一步)颜色
# 玩家定义
EMPTY = 0
PLAYER_BLACK = 1 # 黑棋(人类)
PLAYER_WHITE = 2 # 白棋(AI)
二、棋盘绘制(Pygame)
使用Pygame绘制棋盘和棋子。棋盘是15x15的网格,棋子下在交叉点上。
import pygame
import sys
class GomokuGame:
"""五子棋游戏主类"""
def __init__(self):
pygame.init()
self.screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
pygame.display.set_caption("五子棋 - AI对战")
self.clock = pygame.time.Clock()
self.font = pygame.font.SysFont("simhei", 24)
self.small_font = pygame.font.SysFont("simhei", 16)
# 游戏状态
self.board = [[EMPTY] * BOARD_SIZE for _ in range(BOARD_SIZE)]
self.current_player = PLAYER_BLACK
self.game_over = False
self.winner = None
self.last_move = None # 最后一步落子位置
self.move_count = 0
def draw_board(self):
"""绘制棋盘"""
self.screen.fill(BG_COLOR)
# 绘制棋盘背景
board_rect = pygame.Rect(MARGIN, MARGIN,
BOARD_SIZE * CELL_SIZE,
BOARD_SIZE * CELL_SIZE)
pygame.draw.rect(self.screen, BOARD_COLOR, board_rect)
# 绘制网格线
for i in range(BOARD_SIZE):
# 横线
start = (MARGIN, MARGIN + i * CELL_SIZE)
end = (MARGIN + (BOARD_SIZE - 1) * CELL_SIZE, MARGIN + i * CELL_SIZE)
pygame.draw.line(self.screen, LINE_COLOR, start, end, 1)
# 竖线
start = (MARGIN + i * CELL_SIZE, MARGIN)
end = (MARGIN + i * CELL_SIZE, MARGIN + (BOARD_SIZE - 1) * CELL_SIZE)
pygame.draw.line(self.screen, LINE_COLOR, start, end, 1)
# 绘制天元和星位
star_points = [(3, 3), (3, 11), (7, 7), (11, 3), (11, 11)]
for row, col in star_points:
x = MARGIN + col * CELL_SIZE
y = MARGIN + row * CELL_SIZE
pygame.draw.circle(self.screen, LINE_COLOR, (x, y), 4)
# 绘制棋子
for row in range(BOARD_SIZE):
for col in range(BOARD_SIZE):
if self.board[row][col] != EMPTY:
self.draw_stone(row, col, self.board[row][col])
# 绘制最后一步标记
if self.last_move:
row, col = self.last_move
x = MARGIN + col * CELL_SIZE
y = MARGIN + row * CELL_SIZE
pygame.draw.circle(self.screen, HIGHLIGHT_COLOR, (x, y), 4)
# 绘制状态栏
self.draw_status_bar()
def draw_stone(self, row, col, player):
"""绘制单个棋子"""
x = MARGIN + col * CELL_SIZE
y = MARGIN + row * CELL_SIZE
radius = CELL_SIZE // 2 - 2
color = BLACK if player == PLAYER_BLACK else WHITE
# 棋子阴影效果
pygame.draw.circle(self.screen, (100, 100, 100), (x + 2, y + 2), radius)
pygame.draw.circle(self.screen, color, (x, y), radius)
# 白棋加边框
if player == PLAYER_WHITE:
pygame.draw.circle(self.screen, (150, 150, 150), (x, y), radius, 1)
def draw_status_bar(self):
"""绘制底部状态栏"""
status_y = WINDOW_HEIGHT - 50
pygame.draw.rect(self.screen, (200, 190, 170),
(0, status_y, WINDOW_WIDTH, 50))
if self.game_over:
if self.winner == PLAYER_BLACK:
text = "黑棋获胜!按R重新开始"
elif self.winner == PLAYER_WHITE:
text = "白棋(AI)获胜!按R重新开始"
else:
text = "平局!按R重新开始"
else:
player = "黑棋" if self.current_player == PLAYER_BLACK else "白棋(AI)"
text = f"当前回合:{player} | 已下 {self.move_count} 步 | R重开 ESC退出"
text_surface = self.font.render(text, True, BLACK)
self.screen.blit(text_surface, (10, status_y + 12))
def pixel_to_board(self, pos):
"""将鼠标像素坐标转换为棋盘坐标"""
x, y = pos
col = round((x - MARGIN) / CELL_SIZE)
row = round((y - MARGIN) / CELL_SIZE)
if 0 <= row < BOARD_SIZE and 0 <= col < BOARD_SIZE:
return row, col
return None
三、落子与胜负判定
胜负判定的核心是检查每次落子后,以该点为中心的四个方向(横、竖、左斜、右斜)是否有5连。
def place_stone(self, row, col, player):
"""落子"""
if self.board[row][col] != EMPTY:
return False
self.board[row][col] = player
self.last_move = (row, col)
self.move_count += 1
return True
def check_win(self, row, col, player):
"""检查在 (row, col) 落子后是否获胜"""
# 四个方向:横、竖、左斜、右斜
directions = [(0, 1), (1, 0), (1, 1), (1, -1)]
for dr, dc in directions:
count = 1 # 当前位置算1个
# 正方向延伸
r, c = row + dr, col + dc
while 0 <= r < BOARD_SIZE and 0 <= c < BOARD_SIZE:
if self.board[r][c] == player:
count += 1
r += dr
c += dc
else:
break
# 反方向延伸
r, c = row - dr, col - dc
while 0 <= r < BOARD_SIZE and 0 <= c < BOARD_SIZE:
if self.board[r][c] == player:
count += 1
r -= dr
c -= dc
else:
break
if count >= 5:
return True
return False
def is_board_full(self):
"""检查棋盘是否已满"""
return self.move_count >= BOARD_SIZE * BOARD_SIZE
def reset_game(self):
"""重置游戏"""
self.board = [[EMPTY] * BOARD_SIZE for _ in range(BOARD_SIZE)]
self.current_player = PLAYER_BLACK
self.game_over = False
self.winner = None
self.last_move = None
self.move_count = 0
四、Minimax算法原理
Minimax(极小化极大)算法是博弈论中的经典算法。核心思想是:假设双方都采取最优策略,AI(最大化方)选择能让自己得分最高的走法,而对手(最小化方)会选择让AI得分最低的走法。通过递归模拟未来若干步,评估每个候选位置的优劣。
# Minimax 算法伪代码说明:
#
# def minimax(board, depth, is_maximizing):
# if 游戏结束 or depth == 0:
# return 评估函数(board)
#
# if is_maximizing: # AI(白棋)回合,找最大值
# best_score = -无穷
# for each 可能的落子:
# 落子
# score = minimax(board, depth - 1, False)
# 撤销落子
# best_score = max(best_score, score)
# return best_score
# else: # 对手(黑棋)回合,找最小值
# best_score = +无穷
# for each 可能的落子:
# 落子
# score = minimax(board, depth - 1, True)
# 撤销落子
# best_score = min(best_score, score)
# return best_score
#
# 问题:纯 Minimax 复杂度太高(每个位置有225个候选,深度3就要约1100万次计算)
# 解决:1. 只考虑已有棋子周围的空位(候选剪枝)
# 2. Alpha-Beta 剪枝优化
五、评估函数设计
评估函数是AI强弱的关键。它需要量化一个棋盘局面对于AI的优劣程度。我们通过分析各方向的棋型(连五、活四、冲四、活三、眠三等)来评分。
# 棋型评分表
SCORE_FIVE = 1000000 # 连五(获胜)
SCORE_OPEN_FOUR = 100000 # 活四(下一步必胜)
SCORE_FOUR = 10000 # 冲四(威胁)
SCORE_OPEN_THREE = 1000 # 活三(强威胁)
SCORE_THREE = 100 # 眠三
SCORE_OPEN_TWO = 100 # 活二
SCORE_TWO = 10 # 眠二
SCORE_ONE = 1 # 单子
class Evaluator:
"""棋型评估器"""
def __init__(self):
self.directions = [(0, 1), (1, 0), (1, 1), (1, -1)]
def evaluate_point(self, board, row, col, player):
"""评估在 (row, col) 落子对 player 的价值"""
total_score = 0
for dr, dc in self.directions:
total_score += self.evaluate_direction(
board, row, col, dr, dc, player
)
return total_score
def evaluate_direction(self, board, row, col, dr, dc, player):
"""评估单一方向的棋型"""
# 获取该方向的棋子序列(以落子点为中心)
count = 1 # 当前位置
blocked = 0 # 两端被挡的数量
gap = 0 # 间隔(跳子)
# 正方向
r, c = row + dr, col + dc
consecutive = 0
for i in range(4):
if 0 <= r < BOARD_SIZE and 0 <= c < BOARD_SIZE:
if board[r][c] == player:
consecutive += 1
elif board[r][c] == EMPTY:
break
else: # 对方棋子
blocked += 1
break
r += dr
c += dc
else:
blocked += 1
break
count += consecutive
# 反方向
r, c = row - dr, col - dc
consecutive = 0
for i in range(4):
if 0 <= r < BOARD_SIZE and 0 <= c < BOARD_SIZE:
if board[r][c] == player:
consecutive += 1
elif board[r][c] == EMPTY:
break
else:
blocked += 1
break
r -= dr
c -= dc
else:
blocked += 1
break
count += consecutive
# 根据 count 和 blocked 返回评分
if count >= 5:
return SCORE_FIVE
if count == 4:
if blocked == 0:
return SCORE_OPEN_FOUR # 活四
elif blocked == 1:
return SCORE_FOUR # 冲四
if count == 3:
if blocked == 0:
return SCORE_OPEN_THREE # 活三
elif blocked == 1:
return SCORE_THREE # 眠三
if count == 2:
if blocked == 0:
return SCORE_OPEN_TWO # 活二
elif blocked == 1:
return SCORE_TWO # 眠二
if count == 1:
return SCORE_ONE
return 0
def evaluate_board(self, board, ai_player):
"""评估整个棋盘(用于 Minimax)"""
opponent = PLAYER_WHITE if ai_player == PLAYER_BLACK else PLAYER_BLACK
ai_score = 0
opp_score = 0
for row in range(BOARD_SIZE):
for col in range(BOARD_SIZE):
if board[row][col] == ai_player:
ai_score += self.evaluate_point(board, row, col, ai_player)
elif board[row][col] == opponent:
opp_score += self.evaluate_point(board, row, col, opponent)
# AI 评分减去对手评分,并稍微偏向防守
return ai_score - opp_score * 1.1
六、Alpha-Beta剪枝优化
Alpha-Beta剪枝是Minimax的优化算法,通过维护alpha(当前层已知的最优下界)和beta(当前层已知的最优上界),在搜索过程中提前剪掉不可能产生更优结果的分支,大幅减少计算量。
class GomokuAI:
"""五子棋AI"""
def __init__(self, player=PLAYER_WHITE, max_depth=2):
self.player = player
self.opponent = PLAYER_BLACK if player == PLAYER_WHITE else PLAYER_WHITE
self.max_depth = max_depth
self.evaluator = Evaluator()
def get_candidate_moves(self, board, radius=1):
"""获取候选落子位置(已有棋子周围的空位)"""
candidates = set()
for row in range(BOARD_SIZE):
for col in range(BOARD_SIZE):
if board[row][col] != EMPTY:
# 添加周围 radius 范围内的空位
for dr in range(-radius, radius + 1):
for dc in range(-radius, radius + 1):
r, c = row + dr, col + dc
if (0 <= r < BOARD_SIZE and
0 <= c < BOARD_SIZE and
board[r][c] == EMPTY):
candidates.add((r, c))
return list(candidates)
def minimax(self, board, depth, alpha, beta, is_maximizing):
"""Alpha-Beta 剪枝的 Minimax 算法"""
# 终止条件
if depth == 0:
return self.evaluator.evaluate_board(board, self.player), None
candidates = self.get_candidate_moves(board)
if not candidates:
return self.evaluator.evaluate_board(board, self.player), None
# 按评估分数排序候选位置(提升剪枝效率)
candidates.sort(
key=lambda pos: self.evaluator.evaluate_point(
board, pos[0], pos[1], self.player
),
reverse=True
)
# 限制候选数量(性能优化)
candidates = candidates[:12]
best_move = None
if is_maximizing:
max_eval = float('-inf')
for row, col in candidates:
# 模拟落子
board[row][col] = self.player
# 检查是否直接获胜
if self.check_win_simulate(board, row, col, self.player):
board[row][col] = EMPTY
return SCORE_FIVE * (depth + 1), (row, col)
eval_score, _ = self.minimax(board, depth - 1, alpha, beta, False)
board[row][col] = EMPTY # 撤销
if eval_score > max_eval:
max_eval = eval_score
best_move = (row, col)
alpha = max(alpha, eval_score)
if beta <= alpha:
break # Beta 剪枝
return max_eval, best_move
else:
min_eval = float('inf')
for row, col in candidates:
board[row][col] = self.opponent
if self.check_win_simulate(board, row, col, self.opponent):
board[row][col] = EMPTY
return -SCORE_FIVE * (depth + 1), (row, col)
eval_score, _ = self.minimax(board, depth - 1, alpha, beta, True)
board[row][col] = EMPTY
if eval_score < min_eval:
min_eval = eval_score
best_move = (row, col)
beta = min(beta, eval_score)
if beta <= alpha:
break # Alpha 剪枝
return min_eval, best_move
def check_win_simulate(self, board, row, col, player):
"""模拟检查获胜(不依赖游戏状态)"""
directions = [(0, 1), (1, 0), (1, 1), (1, -1)]
for dr, dc in directions:
count = 1
r, c = row + dr, col + dc
while 0 <= r < BOARD_SIZE and 0 <= c < BOARD_SIZE:
if board[r][c] == player:
count += 1
r += dr
c += dc
else:
break
r, c = row - dr, col - dc
while 0 <= r < BOARD_SIZE and 0 <= c < BOARD_SIZE:
if board[r][c] == player:
count += 1
r -= dr
c -= dc
else:
break
if count >= 5:
return True
return False
def get_best_move(self, board):
"""获取最佳落子位置"""
# 第一步下天元
if all(board[r][c] == EMPTY for r in range(BOARD_SIZE)
for c in range(BOARD_SIZE)):
return (BOARD_SIZE // 2, BOARD_SIZE // 2)
# 检查是否有立即获胜的机会
candidates = self.get_candidate_moves(board)
for row, col in candidates:
board[row][col] = self.player
if self.check_win_simulate(board, row, col, self.player):
board[row][col] = EMPTY
return (row, col)
board[row][col] = EMPTY
# 检查是否需要防守对手的获胜
for row, col in candidates:
board[row][col] = self.opponent
if self.check_win_simulate(board, row, col, self.opponent):
board[row][col] = EMPTY
return (row, col) # 堵住对手
board[row][col] = EMPTY
# 使用 Minimax 搜索
_, best_move = self.minimax(
board, self.max_depth,
float('-inf'), float('inf'),
True
)
return best_move if best_move else candidates[0]
七、AI难度分级
class GomokuAI:
# ... 前面的代码 ...
@classmethod
def create_by_difficulty(cls, difficulty="medium", player=PLAYER_WHITE):
"""根据难度创建AI"""
difficulty_configs = {
"easy": {
"max_depth": 1, # 搜索深度浅
"candidates_limit": 8,
"randomness": 0.3, # 30%概率随机走
"defense_weight": 1.0,
},
"medium": {
"max_depth": 2,
"candidates_limit": 12,
"randomness": 0.1,
"defense_weight": 1.1,
},
"hard": {
"max_depth": 3,
"candidates_limit": 15,
"randomness": 0.0,
"defense_weight": 1.2,
},
"expert": {
"max_depth": 4,
"candidates_limit": 20,
"randomness": 0.0,
"defense_weight": 1.3,
}
}
config = difficulty_configs.get(difficulty, difficulty_configs["medium"])
ai = cls(player=player, max_depth=config["max_depth"])
ai.candidates_limit = config["candidates_limit"]
ai.randomness = config["randomness"]
ai.defense_weight = config["defense_weight"]
return ai
def get_best_move_with_randomness(self, board):
"""带随机性的获取最佳位置(用于简单难度)"""
import random
if random.random() < self.randomness:
# 随机选择一个候选位置
candidates = self.get_candidate_moves(board)
if candidates:
return random.choice(candidates)
return self.get_best_move(board)
八、完整代码:主游戏循环
将以上所有部分组合成完整的游戏。以下为主游戏循环:
import pygame
import sys
class GomokuGame:
"""完整的五子棋游戏"""
def __init__(self, ai_difficulty="medium"):
pygame.init()
self.screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
pygame.display.set_caption("五子棋 - AI对战")
self.clock = pygame.time.Clock()
self.font = pygame.font.SysFont("simhei", 24)
# 游戏状态
self.board = [[EMPTY] * BOARD_SIZE for _ in range(BOARD_SIZE)]
self.current_player = PLAYER_BLACK
self.game_over = False
self.winner = None
self.last_move = None
self.move_count = 0
self.ai_thinking = False
# 创建AI
self.ai = GomokuAI.create_by_difficulty(ai_difficulty, PLAYER_WHITE)
def run(self):
"""主游戏循环"""
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
elif event.key == pygame.K_r:
self.reset_game()
elif event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1 and not self.game_over:
if self.current_player == PLAYER_BLACK:
pos = self.pixel_to_board(event.pos)
if pos:
self.handle_move(pos[0], pos[1])
# AI 回合
if (self.current_player == PLAYER_WHITE and
not self.game_over and not self.ai_thinking):
self.ai_thinking = True
# 在实际项目中可以用线程避免卡顿
move = self.ai.get_best_move(self.board)
if move:
self.handle_move(move[0], move[1])
self.ai_thinking = False
self.draw_board()
pygame.display.flip()
self.clock.tick(30)
pygame.quit()
sys.exit()
def handle_move(self, row, col):
"""处理落子"""
if not self.place_stone(row, col, self.current_player):
return
# 检查胜负
if self.check_win(row, col, self.current_player):
self.game_over = True
self.winner = self.current_player
elif self.is_board_full():
self.game_over = True
self.winner = None # 平局
else:
# 切换玩家
self.current_player = (PLAYER_WHITE
if self.current_player == PLAYER_BLACK
else PLAYER_BLACK)
# ... 其他方法(draw_board, place_stone, check_win 等)见前面 ...
if __name__ == "__main__":
game = GomokuGame(ai_difficulty="medium")
game.run()
九、扩展与改进建议
- 性能优化:使用 Zobrist 哈希实现转置表,避免重复计算相同局面
- 更深的搜索:结合棋型剪枝,可将搜索深度提升至5-6层
- 开局库:预置常见开局走法,提升开局速度和质量
- 多线程:将AI计算放在独立线程,避免阻塞UI
- 蒙特卡洛树搜索(MCTS):替代Minimax,适合更复杂的棋类
- 网络对战:使用 socket 实现双人联机对战
- 悔棋功能:记录走棋历史,支持撤销操作
- 棋谱保存:将对局记录保存为文件,支持复盘
- 禁手规则:实现标准五子棋的黑棋禁手判定(三三、四四、长连)
- 音效与动画:添加落子音效和棋子落下动画
通过本教程,你学习了如何使用Python和Pygame开发五子棋游戏,并实现了基于Minimax算法和Alpha-Beta剪枝的AI对手。这个AI在中等难度下已经能提供不错的挑战,通过调整搜索深度和评估函数,可以进一步提升其棋力。博弈AI是一个深奥而有趣的领域,五子棋是一个很好的入门项目。