FEATURED · 精选文章

函数式编程重构五子棋:从状态模拟到AI实现的迭代优化

发布时间 / 2026/8/21 6:39:00
来源 / 创域科博编辑部
栏目 / 资讯中心
函数式编程重构五子棋:从状态模拟到AI实现的迭代优化 1. 项目概述从“能下棋”到“会下棋”的思维跃迁几年前我写过一个简单的五子棋程序核心逻辑就是用一个二维数组模拟棋盘然后通过遍历来判断胜负。那个版本能跑但代码冗长判断逻辑分散在多个地方维护起来简直是噩梦。最近在带新人做算法训练时我又把这个项目翻了出来决定用函数式编程和迭代优化的思想彻底重构它。这次的目标很明确不仅要实现功能更要写出一份清晰、优雅、易于理解和扩展的代码。这就是“五子棋Version.2 函数模拟迭代解法”的由来。它不是一个简单的游戏复刻而是一次关于如何将复杂问题如棋盘状态判断分解为一系列纯函数并通过迭代优化提升代码质量的实战演练。无论你是刚接触编程的新手想学习如何组织代码逻辑还是有一定经验的开发者希望提升自己的抽象和重构能力这个项目都能给你带来启发。我们将从最朴素的暴力判断开始一步步迭代最终构建一个高效、模块化的五子棋核心引擎。2. 核心设计思路函数式抽象与状态模拟2.1 为何选择函数模拟与迭代解法传统的五子棋AI或游戏逻辑常常会看到庞大的类、错综复杂的条件分支和全局变量。这种写法在初期看似直接但随着规则复杂化例如考虑禁手规则代码会迅速变得难以维护。函数模拟迭代解法的核心思想在于状态不可变性棋盘状态作为一个核心数据比如一个二维列表在判断过程中被视为输入参数函数不会直接修改它而是基于它计算并返回一个新的结果如是否获胜、最佳落子点。这避免了副作用让逻辑更清晰也便于调试和测试。功能单一化将庞大的判断逻辑拆解成一个个功能单一的小函数。例如check_direction只负责在某个方向上连续数子evaluate_position只负责评估某个点的潜在价值。每个函数都像乐高积木通过组合来完成复杂任务。迭代优化我们不追求一蹴而就。第一版实现可能采用最直观但低效的全局扫描。第二版可以引入方向向量减少重复计算。第三版可以加入预计算或缓存。每一次迭代都让代码更快、更清晰这个过程本身极具教学价值。这种范式特别适合五子棋这类基于规则和状态判断的问题。它迫使你思考数据的流动和变换而非纠缠于过程控制。2.2 核心数据模型设计一切始于对棋盘的抽象。我们摒弃复杂的面向对象继承采用最简洁的数据结构。# 核心数据模型棋盘、棋子、位置 BoardType List[List[str]] # 类型别名提高代码可读性 EMPTY . BLACK B WHITE W def create_board(size: int 15) - BoardType: 创建一个指定大小的空棋盘。 return [[EMPTY for _ in range(size)] for _ in range(size)]这里棋盘就是一个二维列表。EMPTY,BLACK,WHITE是常量代表三种格子状态。使用常量而非魔法字符串如直接写‘B’是良好的实践便于统一修改和避免拼写错误。create_board是一个纯函数给定尺寸返回一个新的棋盘状态非常清晰。注意选择15x15是标准五子棋棋盘大小。但在函数式设计中棋盘大小应作为参数传入相关函数使得我们的核心逻辑与具体尺寸解耦更容易适配不同规则如无禁手、有禁手或棋盘大小。3. 核心函数解析与迭代实现3.1 胜负判定函数的迭代之路胜负判定是五子棋的核心。我们将展示三个版本的迭代过程体会优化思路。版本1朴素的全盘扫描法这是最直接的思路每当落下一子就以该子为中心向四个方向横、竖、左斜、右斜检查是否有连续五个同色棋子。def is_win_naive(board: BoardType, row: int, col: int, player: str) - bool: 朴素判定检查刚落子的位置是否构成五连。 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 len(board) and 0 c len(board[0]) and board[r][c] player: count 1 r dr c dc # 向反方向检查 r, c row - dr, col - dc while 0 r len(board) and 0 c len(board[0]) and board[r][c] player: count 1 r - dr c - dc if count 5: return True return False这个函数逻辑正确但存在一个问题它只检查了当前落子点是否形成五连。在标准规则下这足够了。但如果考虑“双三禁手”等复杂规则我们需要知道棋盘上任意位置是否存在五连这个函数就不够用了。版本2基于棋盘状态的通用判定函数为了更通用我们设计一个不依赖特定落子点、只分析当前棋盘状态的函数。def check_five_in_line(board: BoardType, player: str) - bool: 检查指定玩家是否在棋盘上形成了五连。 size len(board) directions [(0, 1), (1, 0), (1, 1), (1, -1)] for r in range(size): for c in range(size): if board[r][c] ! player: continue for dr, dc in directions: count 0 nr, nc r, c # 检查这个方向是否足以构成五连 while 0 nr size and 0 nc size and board[nr][nc] player: count 1 if count 5: return True nr dr nc dc # 一个小优化如果从这个点开始在这个方向上剩余格子数加上已连子数不足5可以提前跳过 # 但为了代码清晰这里暂不实现 return False这个函数会遍历棋盘上的每一个点效率是 O(N^2 * D)其中N是棋盘边长D是方向数4。对于15路棋盘计算量尚可接受但显然不是最优。版本3增量更新与缓存优化高阶迭代在实战中棋盘状态是逐步变化的。我们可以在每次落子后只更新与落子点相关的几个“线”行、列、两条对角线上的连续子信息而不是全盘扫描。这需要维护一个额外的数据结构例如记录每个位置在四个方向上的连续同色子长度。这是一个更高级的优化体现了“以空间换时间”和“状态模拟”的思想。由于篇幅这里给出设计思路定义辅助数据结构direction_maps例如horizontal[r][c]表示在 (r, c) 位置水平向右的连续同色子长度。落子后更新落子点所在行、列、对角线上所有相关格子的direction_maps值。这个更新是局部的。判断胜负时只需检查落子点在四个方向上的连续长度之和正反相加再减1是否达到5。时间复杂度降至 O(1)。这个版本的实现更复杂但它是构建高效AI如需要快速评估数百万个局面的基础。对于教学项目版本2的通用判定函数在清晰度和性能之间取得了很好的平衡。3.2 落子逻辑与状态更新落子不仅仅是放置一个棋子它涉及状态检查、状态更新和触发胜负判定。def make_move(board: BoardType, row: int, col: int, player: str) - tuple[bool, BoardType]: 尝试在指定位置落子。 返回: (是否成功, 新的棋盘状态) 这是一个纯函数返回新的棋盘不修改输入。 size len(board) # 1. 边界和空位检查 if not (0 row size and 0 col size): return False, board if board[row][col] ! EMPTY: return False, board # 2. 创建新的棋盘状态体现不可变性 new_board [row_list[:] for row_list in board] # 浅拷贝每一行 new_board[row][col] player # 3. 胜负检查使用通用判定函数 if check_five_in_line(new_board, player): print(f玩家 {player} 获胜) # 这里可以返回额外的游戏结束状态为了简化我们仅打印 # 注意这里没有修改传入的board而是返回了一个新的 return True, new_board这个make_move函数是纯函数的典范输入是旧状态和操作输出是操作结果和新状态。它没有副作用易于测试。在实际的交互程序中你需要一个外部循环来管理当前的board状态并不断用make_move返回的新状态替换它。实操心得使用[row_list[:] for row_list in board]来拷贝二维列表比copy.deepcopy在性能上更优因为我们的棋盘元素是不可变字符串。对于更复杂的嵌套结构才需要考虑deepcopy。4. 模拟对局与迭代测试框架4.1 构建一个简单的自动对局模拟器为了测试我们的逻辑我们可以构建一个模拟器让两个简单的“AI”随机落子自己下棋直到决出胜负或棋盘下满。这能帮助我们验证游戏终止条件是否正确。import random def simulate_game(board_size: int 15) - dict: 模拟一局随机对局返回对局记录。 board create_board(board_size) players [BLACK, WHITE] current 0 move_history [] total_moves board_size * board_size moves_made 0 while moves_made total_moves: player players[current] # 生成所有空位 empty_positions [ (r, c) for r in range(board_size) for c in range(board_size) if board[r][c] EMPTY ] if not empty_positions: break # 随机选择一个空位落子 r, c random.choice(empty_positions) success, new_board make_move(board, r, c, player) if success: move_history.append((player, r, c)) board new_board # 更新状态 moves_made 1 # 检查是否获胜make_move内部已检查这里可以再断言或记录 if check_five_in_line(board, player): return { winner: player, total_moves: moves_made, history: move_history, final_board: board } current (current 1) % 2 # 切换玩家 else: # 理论上随机选择空位不会失败这里出于健壮性保留 continue # 平局 return { winner: None, total_moves: moves_made, history: move_history, final_board: board } # 运行模拟 if __name__ __main__: result simulate_game(10) # 用小棋盘测试更快 print(f对局结果: 获胜方 {result[winner]}, 总手数 {result[total_moves]})这个模拟器非常有用我们可以运行它成千上万次来统计先手胜率在随机情况下、平均手数或者检查我们的逻辑是否会在某些极端情况下出现无限循环。4.2 迭代测试从正确性到性能有了模拟器我们就可以进行系统的迭代测试。正确性测试用模拟器跑大量对局确保游戏总能正常结束分出胜负或平局并且胜负判定符合人工观察。可以编写一些特定的测试棋盘来验证check_five_in_line函数是否能正确识别边角、长连等情况。性能剖析使用Python的cProfile模块分析在标准15路棋盘上进行大量胜负判定时哪些函数最耗时。你会发现最初的check_five_in_line在全盘遍历时是瓶颈。这直接驱动我们向“版本3”的增量更新方案迭代。压力测试尝试在更大的棋盘如19路上进行模拟观察性能下降是否在可接受范围内。这有助于评估当前算法方案的扩展性。通过这种“实现-测试-分析-优化”的迭代循环代码的质量和你的思考深度都会同步提升。5. 从核心引擎到简单AI的延伸5.1 评估函数的设计一个会下棋的AI核心是能够评估棋盘上某个位置的价值。我们可以设计一个简单的评估函数作为迭代的下一步。def evaluate_position(board: BoardType, row: int, col: int, player: str) - int: 评估在(row, col)位置落子对player的价值。 这是一个非常简化的评估仅计算落子后在该点四个方向上可能形成的最大连子数。 返回一个分数。 if board[row][col] ! EMPTY: return -1000 # 无效位置负分 directions [(0, 1), (1, 0), (1, 1), (1, -1)] total_score 0 for dr, dc in directions: # 模拟落子 count 1 # 正向 r, c row dr, col dc while 0 r len(board) and 0 c len(board[0]) and board[r][c] player: count 1 r dr c dc # 反向 r, c row - dr, col - dc while 0 r len(board) and 0 c len(board[0]) and board[r][c] player: count 1 r - dr c - dc # 给连子数打分连子越多价值指数增长 if count 5: total_score 10000 # 直接获胜 elif count 4: total_score 1000 elif count 3: total_score 100 elif count 2: total_score 10 # count1 几乎不加分 return total_score这个评估函数只考虑了进攻性自己能连多少子。一个更好的评估函数还应该考虑防守性阻止对手形成连子以及棋型活三、冲四、双三等。你可以将其作为下一个迭代目标设计一个能识别常见棋型的函数。5.2 实现一个贪婪AI并组织代码结合评估函数我们可以实现一个最简单的AI贪婪算法即选择当前对自己价值最高的空位落子。def find_best_move_greedy(board: BoardType, player: str) - tuple[int, int]: 贪婪算法选择评估分数最高的空位落子。 best_score -float(inf) best_move (-1, -1) size len(board) for r in range(size): for c in range(size): if board[r][c] EMPTY: # 评估我方在此落子的价值 my_score evaluate_position(board, r, c, player) # 简单起见这里忽略对手的应对。更优的做法是评估对手在此落子的价值并综合考虑。 if my_score best_score: best_score my_score best_move (r, c) return best_move现在我们可以重构主程序将核心函数模块化。一个建议的项目结构如下gobang_v2/ ├── core.py # 核心函数创建棋盘、落子、胜负判定、评估 ├── ai.py # AI逻辑贪婪算法、后续可扩展Minimax等 ├── simulator.py # 模拟对局、测试 └── main.py # 主程序可选图形界面或命令行界面入口在main.py中你可以组织一个简单的人机对战循环# main.py 示例片段 from core import create_board, make_move, check_five_in_line from ai import find_best_move_greedy def human_vs_ai(): board create_board(15) current_player BLACK # 人类执黑先行 while True: # 打印棋盘简单的文本打印 for row in board: print( .join(row)) print() if current_player BLACK: # 人类回合 try: r, c map(int, input(请输入您的落子位置 (行 列从0开始): ).split()) except ValueError: print(输入无效请重新输入。) continue else: # AI回合执白 print(AI正在思考...) r, c find_best_move_greedy(board, WHITE) print(fAI落子于 ({r}, {c})) success, new_board make_move(board, r, c, current_player) if not success: print(该位置不可落子请重试。) continue board new_board if check_five_in_line(board, current_player): for row in board: print( .join(row)) print(f玩家 {current_player} 获胜) break # 切换玩家 current_player WHITE if current_player BLACK else BLACK if __name__ __main__: human_vs_ai()6. 常见问题、调试技巧与迭代方向6.1 开发中遇到的典型问题棋盘索引越界在方向搜索循环中while条件的边界检查0 r size至关重要。忘记检查会导致列表索引错误。调试技巧在循环开始时打印(r, c)的值或者使用断言assert 0 r size。状态污染这是函数式编程要解决的核心问题。如果你不小心直接修改了传入的board例如使用board[row][col] player会导致难以追踪的bug。强制习惯在所有修改状态的函数里第一行就写new_board [row[:] for row in old_board]并坚持操作new_board。低效的全盘扫描在check_five_in_line的版本2中即使棋盘大部分为空也会遍历所有格子。性能优化可以维护一个“最后落子点”列表只检查这些点周围的区域或者直接采用增量更新方案版本3。评估函数片面最初的evaluate_position只考虑进攻AI会显得很有攻击性但不会防守。解决方案将评估函数改为score my_score - opponent_score * factor其中factor是一个防守权重系数通过大量模拟对局来调整这个系数找到攻守平衡点。6.2 项目迭代路线图这个五子棋项目可以像一个产品一样持续迭代迭代1基础完成当前版本实现纯函数化的核心逻辑、正确胜负判定、简单模拟器和贪婪AI。迭代2优化实现增量更新的胜负判定版本3大幅提升性能。引入基础棋型活二、死三、活三等识别到评估函数中。迭代3智能实现 Minimax 搜索算法并加入 Alpha-Beta 剪枝。让AI能够思考未来几步棋而不仅仅是眼前一步。迭代4工程化添加图形用户界面如使用 PyGame 或 Tkinter完善人机交互。引入开局库、Zobrist 哈希用于局面去重和缓存。迭代5高级尝试实现蒙特卡洛树搜索MCTS这是现代围棋AI的基石之一在五子棋上也能取得很好效果。每一次迭代都不是推倒重来而是在原有清晰模块的基础上添加新功能。例如在迭代3中你只需要修改ai.py中的find_best_move函数将其内部实现从贪婪算法替换为 Minimax 搜索而core.py中的棋盘和判定函数可以完全复用。6.3 一个实用的调试技巧可视化棋盘状态在调试复杂逻辑如评估函数或搜索算法时纯文本输出不够直观。可以写一个简单的可视化函数用不同字符或颜色高亮显示AI正在评估的落子点、连子等。def print_board_highlight(board: BoardType, highlight_pos: set None): 打印棋盘并可高亮特定位置。 if highlight_pos is None: highlight_pos set() size len(board) print( .join(str(i).rjust(2) for i in range(size))) for r in range(size): row_str str(r).rjust(2) for c in range(size): cell board[r][c] if (r, c) in highlight_pos: cell f\033[91m{cell}\033[0m # 红色高亮仅支持部分终端 row_str cell print(row_str) # 在AI思考时调用高亮它正在评估的候选点 candidate_moves [(7,7), (7,8), (8,7)] print_board_highlight(board, set(candidate_moves))通过这个项目你收获的不仅仅是一个五子棋程序更是一套处理复杂逻辑问题的函数式思维方法和迭代开发的心智模型。从最笨拙但正确的代码开始一步步分析瓶颈、抽象功能、优化结构最终得到一个既优雅又高效的作品这个过程本身其价值远超代码本身。当你下次面对其他复杂系统时你会自然而然地思考“它的核心状态是什么可以拆分成哪些纯函数如何设计数据流” 这才是这个项目带给你的真正武器。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻