返回文章列表

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()

九、扩展与改进建议

通过本教程,你学习了如何使用Python和Pygame开发五子棋游戏,并实现了基于Minimax算法和Alpha-Beta剪枝的AI对手。这个AI在中等难度下已经能提供不错的挑战,通过调整搜索深度和评估函数,可以进一步提升其棋力。博弈AI是一个深奥而有趣的领域,五子棋是一个很好的入门项目。

动手挑战

学到这里,不妨动手试一试以下练习,巩固你的理解:

  1. 基础练习:回顾本文核心概念,用自己的话总结关键知识点。
  2. 进阶实践:将文中的示例代码运行一遍,尝试修改参数观察变化。
  3. 拓展思考:想一想这个技术/方法还能应用在哪些场景中?

小贴士:遇到问题时,先独立思考,再查阅资料,最后请教他人——这是成长最快的学习方式。

赞赏支持

本文更新于 2026-08-22,环境 Python 3.12