本专栏的第二篇和第四篇文章分别介绍了交互友好的五子棋游戏、支持AI人机对战的五子棋游戏,具体内容可以参考以下两篇帖子:

CS课程项目设计2:交互友好的五子棋游戏-CSDN博客https://blog.csdn.net/weixin_36431280/article/details/149415017?spm=1001.2014.3001.5501CS课程项目设计4:支持AI人机对战的五子棋游戏-CSDN博客https://blog.csdn.net/weixin_36431280/article/details/149432389?spm=1001.2014.3001.5501

这两个项目实现的五子棋棋盘都太简陋了,不太美观。因此,和本专栏的第五篇和第六篇文章一样:

CS课程项目设计5:基于Canvas交互友好的井字棋游戏-CSDN博客https://blog.csdn.net/weixin_36431280/article/details/149664504?spm=1001.2014.3001.5501

CS课程项目设计6:基于Canvas支持人机对战的井字棋游戏-CSDN博客https://blog.csdn.net/weixin_36431280/article/details/149668934?spm=1001.2014.3001.5501

我们使用Canvas组件进一步改进这两个项目,提高页面布局的美观度。

原始支持AI人机对战的五子棋游戏棋盘和现有基于Canvas支持AI人机对战的五子棋游戏棋盘的对比图如下所示:


1. 研究目的

这个项目重新设计了棋盘、按键、设置玩家昵称页面、赢局和平局显示页面,并且对照本专栏第二篇文章来介绍这个基于Canvas支持AI人机对战的五子棋游戏。

具体而言,为了让五子棋棋盘更加美观,我们可以使用 tkinter 的 Canvas 组件来绘制棋盘,并用 Canvas 上的图形表示棋子,同时可以调整颜色和线条样式来提升视觉效果。

主要修改点:

  1. 使用Canvas绘制棋盘:通过Canvas组件绘制棋盘网格,使用create_line方法绘制横线和竖线,让棋盘更加美观。
  2. 使用Canvas绘制棋子:使用create_oval方法在指定位置绘制圆形棋子,根据玩家的不同设置不同的颜色。
  3. 处理画布点击事件:通过绑定<Button-1>事件,处理鼠标点击画布的操作,计算点击位置对应的棋盘坐标,并调用make_move方法处理落子。
  4. 修改悔棋和加载游戏时的界面更新逻辑:在悔棋和加载游戏时,需要清空画布并重新绘制棋盘和棋子。

同时,使用 Tkinter 构建的更美观的五子棋用户名称设置界面。这个界面保留了原有的功能,同时采用了更现代的设计风格,包括自定义按钮、文本输入框和布局,具体改进如下所示:

  • 更美观的界面设计,包括背景色、字体和布局
  • 自定义按钮样式,带有悬停效果和圆角
  • 针对不同游戏模式(人机对战和双人对战)的个性化界面
  • 更好的窗口定位和大小控制
  • 输入框中的默认值显示当前玩家名称
  • 在人机模式下,AI 名称不可编辑

最后,对设置用户昵称和显示玩家胜利的布局进行了美化,使用了更现代的设计风格和交互效果。主要改进包括:

  • 为设置用户昵称创建了自定义对话框
  • 使用更现代的字体和颜色方案
  • 为胜利消息添加了动画效果和视觉反馈
  • 优化了整体布局,增加了间距和对齐
  • 为玩家状态添加了颜色标记
  • 添加了平滑过渡效果

2. 技术方案

2.1. 图形用户界面(GUI)

使用 Python 的tkinter库创建游戏的图形用户界面,包括棋盘按钮、玩家信息标签、状态标签和操作按钮等。

本游戏主要设置了双人对战和人机对战两种对战模式,在双人对战模式中,你需要分别设置你和对手的用户昵称,可视化界面如下图所示:

在人机对战模式中,你只需要设置你的用户昵称,可视化界面如下图所示:

2.2. 游戏逻辑实现

通过二维列表self.board来表示棋盘状态,使用self.current_player来记录当前玩家。在玩家或 AI 落子后,调用check_winner方法检查是否有玩家获胜,调用is_board_full方法检查棋盘是否已满。

2.3. AI 算法

采用极小极大算法(Minimax Algorithm)并结合Alpha - Beta 剪枝技术来实现 AI 的落子决策。根据不同的 AI 难度设置不同的搜索深度,以平衡 AI 的计算时间和决策质量。同时,为了避免 AI 计算超时,设置了self.ai_timeout参数,当计算时间超过该值时,AI 会随机选择一个空位落子。

2.4. 游戏状态保存和加载

使用 JSON 格式将游戏状态(如棋盘状态、当前玩家、落子历史等)保存到文件中,方便后续加载游戏进度。

2.5. 音效和动画

使用playsound库在玩家落子、获胜、平局和悔棋等情况下播放相应的音效。通过tkinterupdate方法和time.sleep函数实现棋子放置和获胜动画效果。

3. 实现流程

3.1. 初始化

Gomoku类的__init__方法中,初始化游戏的各种属性,包括棋盘大小、当前玩家、玩家名称、AI 难度等。同时,创建游戏界面组件,如棋盘按钮、状态标签和操作按钮等,并允许用户设置玩家名称。

我们实现了三种AI难度级别(简单、中等、困难),满足不同技能水平玩家的需求,可视化界面如下图所示:

3.2. 玩家落子

当玩家点击棋盘上的按钮时,调用make_move方法处理玩家的落子操作。该方法会更新棋盘状态、检查游戏是否结束,并切换玩家。如果当前游戏模式为人机对战且轮到 AI 玩家,则调用ai_make_move方法让 AI 落子。

其中,处理玩家点击事件,更新棋盘状态的make_move代码如下所示:

def make_move(self, row, col):
    """处理玩家移动"""
    if self.board[row][col] == ' ' and self.game_active:
        # 记录当前移动到历史
        self.move_history.append((row, col, self.current_player))
        self.undo_button.config(state=tk.NORMAL)  # 启用悔棋按钮
 
        # 播放放置音效
        self.play_sound('place')
 
        # 添加放置动画
        self.animate_cell(row, col)
 
        # 更新棋盘数据
        self.board[row][col] = self.current_player
        # 更新按钮显示(使用●表示黑棋,○表示白棋)
        symbol = '●' if self.current_player == 'X' else '○'
        self.buttons[row][col].config(text=symbol)
        # 记录上一步
        self.last_move = (row, col)
        self.last_move_label.config(
            text=f"上一步: {self.player_names[self.current_player]} 在位置 {row + 1},{col + 1}"
        )
        # 检查游戏状态
        if self.check_winner(self.current_player):
            self.status_label.config(text=f"{self.player_names[self.current_player]} 获胜!")
            self.game_active = False
            self.undo_button.config(state=tk.DISABLED)  # 禁用悔棋按钮
 
            # 播放胜利音效和动画
            self.play_sound('win')
            self.animate_winning_cells()
 
            messagebox.showinfo("游戏结束", f"{self.player_names[self.current_player]} 获胜!")
        elif self.is_board_full():
            self.status_label.config(text="游戏平局!")
            self.game_active = False
            self.undo_button.config(state=tk.DISABLED)  # 禁用悔棋按钮
 
            # 播放平局音效
            self.play_sound('draw')
 
            messagebox.showinfo("游戏结束", "游戏平局!")
        else:
            # 切换玩家
            self.current_player = 'O' if self.current_player == 'X' else 'X'
            self.status_label.config(text=f"当前玩家: {self.player_names[self.current_player]}")
 
            # 如果是AI玩家且游戏模式为人机对战,则AI落子
            if self.game_mode == 'human_vs_ai' and self.current_player == self.ai_player:
                self.status_label.config(text=f"{self.player_names[self.current_player]} 正在思考...")
                self.root.after(500, self.ai_make_move)  # 延迟500ms开始AI思考,避免界面卡顿

调用ai_make_move方法让 AI 落子的代码如下所示:

def ai_make_move(self):
    """AI落子"""
    if not self.game_active or self.game_mode != 'human_vs_ai' or self.current_player != self.ai_player:
        return
 
    self.ai_start_time = time.time()
    self.best_move = None
 
    # 根据难度选择不同的搜索深度
    depth = 1 if self.ai_difficulty == 0 else 3 if self.ai_difficulty == 1 else 5
    self.initial_depth = depth  # 保存初始深度用于记录最佳落子
 
    # 使用极小极大算法寻找最佳落子
    self.minimax(depth, -float('inf'), float('inf'), True)
 
    # 检查是否超时,如果超时则随机选择一个空位
    current_time = time.time()
    if current_time - self.ai_start_time > self.ai_timeout or self.best_move is None:
        # 随机选择一个空位
        empty_cells = [(r, c) for r in range(self.board_size) for c in range(self.board_size) if
                       self.board[r][c] == ' ']
        if empty_cells:
            self.best_move = random.choice(empty_cells)
 
    # 执行落子
    if self.best_move:
        row, col = self.best_move
        self.make_move(row, col)

可视化界面如下所示:

3.3. AI 落子

ai_make_move方法中,根据 AI 难度设置搜索深度,调用minimax方法使用极小极大算法和 Alpha - Beta 剪枝技术寻找最佳落子位置。如果计算超时或未找到最佳位置,则随机选择一个空位落子。

3.4. 极小极大算法

minimax方法实现了极小极大算法和 Alpha - Beta 剪枝技术。在递归过程中,根据当前玩家的角色(最大化或最小化)选择最优的落子位置。同时,在每次递归调用时检查是否超时,如果超时则提前返回结果。该算法的代码如下所示:

def minimax(self, depth, alpha, beta, is_maximizing):
    """极小极大算法,带alpha-beta剪枝和超时检查"""
    # 检查超时
    current_time = time.time()
    if current_time - self.ai_start_time > self.ai_timeout:
        return -1000 if is_maximizing else 1000
 
    # 检查游戏状态
    if self.check_winner(self.ai_player):
        return 10000 + depth  # AI获胜
    if self.check_winner('X' if self.ai_player == 'O' else 'O'):
        return -10000 - depth  # 玩家获胜
    if self.is_board_full():
        return 0  # 平局
 
    if depth == 0:
        return self.evaluate_board()  # 评估当前局面
 
    if is_maximizing:
        max_score = -float('inf')
        # 获取所有空位并按分数排序(启发式排序)
        empty_cells = [(r, c) for r in range(self.board_size) for c in range(self.board_size) if
                       self.board[r][c] == ' ']
        # 按中心距离排序,优先考虑中心区域
        empty_cells.sort(key=lambda pos: abs(pos[0] - 7) + abs(pos[1] - 7))
 
        for r, c in empty_cells:
            self.board[r][c] = self.ai_player
            score = self.minimax(depth - 1, alpha, beta, False)
            self.board[r][c] = ' '
 
            if score > max_score:
                max_score = score
                if depth == self.initial_depth:  # 记录最佳落子位置
                    self.best_move = (r, c)
 
            alpha = max(alpha, score)
            if beta <= alpha:
                break  # Beta剪枝
 
            # 检查超时
            current_time = time.time()
            if current_time - self.ai_start_time > self.ai_timeout:
                return max_score
 
        return max_score
    else:
        min_score = float('inf')
        # 获取所有空位并按分数排序(启发式排序)
        empty_cells = [(r, c) for r in range(self.board_size) for c in range(self.board_size) if
                       self.board[r][c] == ' ']
        # 按中心距离排序,优先考虑中心区域
        empty_cells.sort(key=lambda pos: abs(pos[0] - 7) + abs(pos[1] - 7))
 
        for r, c in empty_cells:
            opponent = 'X' if self.ai_player == 'O' else 'O'
            self.board[r][c] = opponent
            score = self.minimax(depth - 1, alpha, beta, True)
            self.board[r][c] = ' '
 
            if score < min_score:
                min_score = score
 
            beta = min(beta, score)
            if beta <= alpha:
                break  # Alpha剪枝
 
            # 检查超时
            current_time = time.time()
            if current_time - self.ai_start_time > self.ai_timeout:
                return min_score
 
        return min_score

3.5. 评估函数

evaluate_board方法用于评估当前棋盘状态的分数,根据 AI 和玩家的连续棋子数给予不同的分数奖励或惩罚,同时对中心位置的棋子给予额外的奖励或惩罚。该算法的代码如下所示:

def evaluate_board(self):
    """评估棋盘状态,返回分数"""
    score = 0
    ai_player = self.ai_player
    human_player = 'X' if ai_player == 'O' else 'O'
 
    # 评估行、列、对角线
    directions = [(0, 1), (1, 0), (1, 1), (1, -1)]
 
    for row in range(self.board_size):
        for col in range(self.board_size):
            if self.board[row][col] == ' ':
                continue
 
            # 检查当前位置在各个方向上的连续棋子
            for dx, dy in directions:
                # 计算当前方向上AI和玩家的连续棋子数
                ai_count, human_count = self.count_consecutive(row, col, dx, dy)
 
                # 根据连续棋子数给予分数
                if ai_count > 0:
                    if ai_count >= 5:  # AI五连
                        score += 100000
                    elif ai_count == 4:  # AI四连
                        score += 10000
                    elif ai_count == 3:  # AI三连
                        score += 1000
                    elif ai_count == 2:  # AI二连
                        score += 100
                    else:  # AI一连
                        score += 10
 
                if human_count > 0:
                    if human_count >= 5:  # 玩家五连
                        score -= 100000
                    elif human_count == 4:  # 玩家四连
                        score -= 10000
                    elif human_count == 3:  # 玩家三连
                        score -= 1000
                    elif human_count == 2:  # 玩家二连
                        score -= 100
                    else:  # 玩家一连
                        score -= 10
 
    # 中心位置奖励
    center = self.board_size // 2
    for r in range(center - 1, center + 2):
        for c in range(center - 1, center + 2):
            if 0 <= r < self.board_size and 0 <= c < self.board_size:
                if self.board[r][c] == ai_player:
                    score += 5
                elif self.board[r][c] == human_player:
                    score -= 5
 
    return score

3.6. 游戏状态保存和加载

save_game方法将游戏状态保存为 JSON 文件,load_game方法从 JSON 文件中加载游戏状态并恢复。

3.7. 悔棋功能

undo_move方法实现了悔棋功能,将上一步的落子撤销,恢复棋盘状态和玩家信息。悔棋也是一个关键功能,实现代码如下所示:

def undo_move(self):
    """悔棋功能"""
    if not self.move_history:
        return  # 没有历史记录
 
    # 播放悔棋音效
    self.play_sound('undo')
 
    # 恢复上一步
    row, col, player = self.move_history.pop()
    self.board[row][col] = ' '
    self.buttons[row][col].config(text='', bg='SystemButtonFace')  # 恢复默认背景
 
    # 清除获胜高亮
    if self.winning_cells:
        for r, c in self.winning_cells:
            self.buttons[r][c].config(bg='SystemButtonFace')
        self.winning_cells = []
 
    # 更新上一步信息
    if self.move_history:
        last_row, last_col, last_player = self.move_history[-1]
        self.last_move = (last_row, last_col)
        self.last_move_label.config(
            text=f"上一步: {self.player_names[last_player]} 在位置 {last_row + 1},{last_col + 1}"
        )
    else:
        self.last_move = None
        self.last_move_label.config(text="上一步: 无")
 
    # 切换回上一个玩家
    self.current_player = player
    self.status_label.config(text=f"当前玩家: {self.player_names[self.current_player]}")
 
    # 重新激活游戏(如果之前结束了)
    self.game_active = True
 
    # 如果没有历史记录了,禁用悔棋按钮
    if not self.move_history:
        self.undo_button.config(state=tk.DISABLED)

可视化界面如下所示:

3.8. 重置游戏

reset_game方法将游戏状态重置为初始状态,清空棋盘和落子历史。

4. 总结

本代码成功实现了一个支持人机对战的五子棋游戏,具备友好的图形用户界面、多种游戏模式和 AI 难度选择、游戏状态保存和加载、悔棋功能以及音效和动画效果。通过使用极小极大算法和 Alpha - Beta 剪枝技术,AI 能够做出合理的落子决策,为玩家提供了一定的挑战。同时,代码的结构清晰,各个功能模块分工明确,便于后续的扩展和维护。然而,代码也存在一些可以改进的地方,例如可以进一步优化 AI 的评估函数,提高 AI 的决策质量;可以添加更多的游戏规则和玩法,增加游戏的趣味性。

5. 项目展示

最后上传个该项目的简要演示视频,供大家了解。

基于Canvas支持AI人机对战的五子棋游戏

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐