尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Python五子棋实战:从二维列表到AI对弈的完整项目指南

Python五子棋实战:从二维列表到AI对弈的完整项目指南 1. 项目缘起为什么用Python写五子棋如果你刚开始学Python或者已经学了一段时间语法但总觉得那些变量、循环、列表、函数学完了不知道能干嘛那这个项目就太适合你了。用Python写一个简易的五子棋游戏听起来好像挺复杂但实际上它几乎用到了你学过的所有核心语法并且把它们串联成了一个看得见、摸得着的“作品”。这比单纯在控制台打印“Hello World”或者算个数学题要有成就感得多。我当初学Python时也经历过这个阶段。看教程觉得都懂了一上手写个稍微综合点的东西就懵了。后来我决定从游戏入手五子棋规则简单逻辑清晰棋盘用二维列表就能完美表示胜负判断就是遍历和条件判断的经典应用。整个过程下来你对Python的理解会从“知识点”层面跃升到“项目构建”层面。你会明白一个程序是如何从零开始一步步把想法变成可运行的代码的。今天我就把我当时实现这个项目的完整思路、代码细节以及那些教程里不会告诉你的“坑”和技巧毫无保留地分享给你。即使你是个纯新手跟着这篇文章一步步走也能拥有一个属于你自己的、能在命令行里对战的五子棋程序。2. 核心设计如何用代码模拟一个棋盘写任何游戏第一步都是建立它的“世界模型”。对于五子棋这个世界就是一个15x15的网格棋盘以及两个轮流下棋的玩家。在代码里我们怎么表示它呢2.1 棋盘的数据结构选择最直观、也最常用的方法就是使用一个二维列表。你可以把它想象成一个Excel表格有行有列。我们创建一个15行、15列的列表每个格子初始时都是空的。# 初始化一个15x15的棋盘所有位置用 表示空位 BOARD_SIZE 15 board [[ for _ in range(BOARD_SIZE)] for _ in range(BOARD_SIZE)]这里有几个细节需要注意BOARD_SIZE 15用一个常量来表示棋盘大小是个好习惯。如果你想改成13路或19路棋盘只需要改这一个地方后面的代码会自动适应。这比把数字15写死在代码各处要优雅和安全得多。列表推导式[‘’ for _ in range(BOARD_SIZE)]这是快速创建列表的语法。外层的推导式创建了15行内层的推导式为每一行创建了15个元素每个元素初始化为‘’。我选择用加号表示空位因为它看起来像个十字交叉点比较形象。你也可以用空格‘ ’或点‘.’。for _ in …中的下划线这是一个约定俗成的做法表示我们在循环中不需要用到这个循环变量只是让它执行特定次数。用下划线比用i或j更能表达“忽略此变量”的意图。为什么不用一维列表或者字典二维列表在表示网格状数据时其索引board[row][col]能非常直观地对应到棋盘的“第row行第col列”访问和修改某个特定位置的状态极其方便。这是最匹配我们思维模型的数据结构。2.2 玩家的符号与回合逻辑五子棋有两个玩家通常用黑棋和白棋。在命令行里我们用不同的字符来代表它们比如‘X’代表黑棋‘O’代表白棋或者用‘●’和‘○’如果终端支持Unicode。回合制是另一个核心。我们需要一个变量来记录当前该谁下棋。current_player ‘X’ # 黑棋先行每走完一步我们就切换这个玩家current_player ‘O’ if current_player ‘X’ else ‘X’这行代码用了条件表达式意思是如果当前玩家是‘X’那么下一个玩家就是‘O’否则当前是‘O’下一个玩家就是‘X’。它等价于一个if-else语句但更简洁。3. 功能实现一绘制棋盘与落子有了数据模型接下来就要让用户能看见并能与之交互。3.1 打印棋盘函数我们需要一个函数能把内存里的二维列表board以人类看得懂的方式打印到屏幕上。def print_board(board): # 打印列号0-14方便用户输入坐标 print(‘ ‘ ‘ ‘.join([str(i).rjust(2) for i in range(BOARD_SIZE)])) # 遍历每一行 for row in range(BOARD_SIZE): # 打印行号同样右对齐保持美观 print(str(row).rjust(2), end‘ ‘) # 打印这一行的所有棋子状态用空格隔开 print(‘ ‘.join(board[row]))这里用到了几个技巧str(i).rjust(2)rjust(2)是字符串的右对齐方法确保每个数字占2个字符宽度。这样打印出来的列号会对齐棋盘看起来更整齐。‘ ‘.join(…)join方法用指定的字符串这里是空格连接一个列表的所有元素。‘ ‘.join(board[row])就是把第row行的15个字符用空格连成一个字符串打印出来。print(…, end‘ ‘)默认print会换行。这里end‘ ‘指定打印完行号后以空格结尾而不是换行紧接着在同一行打印棋子内容。运行print_board(board)你会看到一个整齐的、带坐标的空白棋盘。这是你和程序交互的视觉基础。3.2 处理玩家输入与落子玩家需要通过输入坐标来下棋比如输入“7,7”表示在棋盘正中央落子。我们需要一个函数来处理这个输入并更新棋盘。def make_move(board, row, col, player): 在指定位置放置玩家的棋子 if board[row][col] ! ‘’: return False # 该位置已有棋子落子失败 board[row][col] player return True # 落子成功这个函数很简单检查目标位置是否为空‘’如果是就放上当前玩家的棋子并返回成功否则返回失败。关键在于如何把用户输入的“7,7”转换成函数需要的row7, col7这需要在主循环里处理while True: print_board(board) print(f“当前玩家: {current_player}“) try: # 假设用户输入格式为 ‘行,列’例如 ‘7,7’ input_str input(“请输入落子位置 (行,列): “) row_str, col_str input_str.split(‘,’) row, col int(row_str.strip()), int(col_str.strip()) # 检查坐标是否在棋盘范围内 if 0 row BOARD_SIZE and 0 col BOARD_SIZE: if make_move(board, row, col, current_player): # 落子成功检查胜负然后切换玩家 if check_win(board, row, col, current_player): print_board(board) print(f“玩家 {current_player} 获胜”) break current_player ‘O’ if current_player ‘X’ else ‘X’ else: print(“该位置已有棋子请重新选择“) else: print(f“坐标超出范围请输入0到{BOARD_SIZE-1}之间的数字。“) except (ValueError, IndexError): print(“输入格式错误请按 ‘行,列’ 的格式输入例如 ‘7,7’。“)注意这里使用了try…except来捕获异常。用户输入是不可靠的他可能输入“a,b”可能只输入一个数字也可能用空格或其他符号分割。split(‘,’)可能引发IndexError如果没逗号int()转换可能引发ValueError如果不是数字。用异常处理可以保证程序不会因为用户的错误输入而崩溃而是给出友好的提示。这是编写健壮程序的基本素养。4. 功能实现二胜负判断的逻辑核心这是五子棋项目的算法核心也是最考验逻辑思维的部分。胜负判断的本质是在最新落子点(row, col)的四个方向水平、垂直、两条对角线上检查是否存在连续五个相同的棋子。4.1 方向向量的运用最清晰的方法不是写四段冗长相似的代码而是使用“方向向量”。一个方向向量(dr, dc)表示在行和列上的移动步长。水平方向(0, 1)表示向右移动(0, -1)表示向左移动。垂直方向(1, 0)表示向下移动(-1, 0)表示向上移动。主对角线\(1, 1)表示向右下移动(-1, -1)表示向左上移动。副对角线/(1, -1)表示向左下移动(-1, 1)表示向右上移动。这样我们只需要定义这四个方向对DIRECTIONS [(0, 1), (1, 0), (1, 1), (1, -1)]每个方向对都包含了该方向上的两个相反向量。例如(0, 1)和(0, -1)共同构成了水平线。4.2 检查单方向连续棋子数我们写一个辅助函数给定一个起始点和方向向量计算这个方向上连续相同棋子的数量包括起始点本身。def count_in_direction(board, start_row, start_col, dr, dc, player): 从(start_row, start_col)开始沿(dr, dc)方向计算连续属于player的棋子数量。 count 0 r, c start_row, start_col # 向一个方向不断探索直到走出棋盘或棋子不匹配 while 0 r BOARD_SIZE and 0 c BOARD_SIZE and board[r][c] player: count 1 r dr c dc return count4.3 整合胜负判断函数现在我们可以在落子点检查四个方向了。对于每个方向对我们把两个相反方向上的连续棋子数加起来但因为起始点被计算了两次所以要减去1。如果任何一个方向的总数达到5则获胜。def check_win(board, row, col, player): 检查在(row, col)处落子后玩家player是否获胜。 for dr, dc in DIRECTIONS: # 计算正方向上的连续棋子数 count_forward count_in_direction(board, row, col, dr, dc, player) # 计算反方向上的连续棋子数 count_backward count_in_direction(board, row, col, -dr, -dc, player) # 总连续数 正向数 反向数 - 1 (因为中心点被算了两次) if (count_forward count_backward - 1) 5: return True return False这个算法的效率很高因为它只检查了最新的落子点而不是每次都对整个棋盘进行全局扫描。其时间复杂度是常数级的与棋盘大小无关。实操心得在测试这个函数时最容易出现的bug是“边界判断”。确保你的count_in_direction函数中的while循环条件0 r BOARD_SIZE是先判断索引有效性再访问board[r][c]。如果顺序反了当r或c超出范围时Python会先尝试访问board[r][c]从而引发IndexError索引越界错误。逻辑运算符and是短路求值的条件顺序很重要。5. 功能实现三让电脑成为你的对手简易AI只有两个人对战还不够我们来实现一个简单的电脑对手。这个AI不会思考它只会做两件事1. 尝试自己连成五子获胜2. 如果没有获胜点就随机找一个空位落子。这虽然简单但已经能让游戏变得有趣。5.1 随机落子策略这是最简单的AI策略作为我们AI的“保底”行为。import random def get_random_move(board): 获取一个随机空位作为落子点。 empty_positions [ (r, c) for r in range(BOARD_SIZE) for c in range(BOARD_SIZE) if board[r][c] ‘’ ] if empty_positions: return random.choice(empty_positions) else: return None # 棋盘已满平局这里用到了列表推导式来收集所有空位坐标代码非常简洁。random.choice会从列表中随机选择一个返回。5.2 获胜点搜索策略在随机下棋之前我们应该让AI先“聪明”一点检查一下是否有一步就能让自己获胜的位置。这只需要模拟一下遍历所有空位假设自己在那里落子然后调用check_win函数判断是否能赢。def find_winning_move(board, player): 寻找玩家player一步制胜的落子点。 for r in range(BOARD_SIZE): for c in range(BOARD_SIZE): if board[r][c] ‘’: # 是空位 # 模拟落子 board[r][c] player if check_win(board, r, c, player): board[r][c] ‘’ # 撤销模拟落子 return (r, c) board[r][c] ‘’ # 撤销模拟落子 return None注意这里“模拟-检查-撤销”的模式。我们修改了棋盘状态来进行胜负判断但判断完后必须立刻恢复原状否则棋盘就被污染了。这是一个常见的测试技巧。5.3 整合AI决策函数现在把两种策略组合起来形成一个有优先级的AI先找获胜点找不到就随机下。def get_computer_move(board, computer_player): 获取电脑的落子位置。 # 策略1: 检查电脑自己是否有一步获胜的机会 move find_winning_move(board, computer_player) if move: return move # 策略2: 随机落子 move get_random_move(board) return move你可以修改主循环让玩家和这个简易AI对战。只需要在轮到电脑时调用get_computer_move函数获取坐标然后执行make_move。if current_player computer_player: print(“电脑思考中...“) row, col get_computer_move(board, computer_player) if row is None: # 棋盘已满 print(“棋盘已满平局“) break make_move(board, row, col, computer_player) # ... 检查胜负和切换玩家 else: # ... 人类玩家输入逻辑踩坑提醒在实现AI时我最初犯过一个错误在find_winning_move函数里找到获胜点后我直接return (r, c)却忘了把模拟落下的棋子撤销。这导致主循环中真正落子时棋盘上那个位置已经有一个棋子了虽然打印出来可能看不到因为被覆盖了导致make_move失败。记住任何对共享数据如全局棋盘的临时修改都必须确保在函数退出前恢复到原始状态除非这就是你的本意。6. 项目整合与代码优化现在我们把所有模块组合在一起形成一个完整的、可以人机对战的五子棋游戏。同时我会分享几个让代码更健壮、更易用的优化点。6.1 主游戏循环的完整结构一个结构清晰的主循环是程序可读性的关键。def main(): # 1. 初始化 board [[ for _ in range(BOARD_SIZE)] for _ in range(BOARD_SIZE)] human_player ‘X’ computer_player ‘O’ current_player human_player # 人类先行 print(“欢迎来到简易五子棋“) print(“您将执X电脑执O。请输入坐标格式为‘行,列’例如‘7,7’。“) # 2. 游戏主循环 while True: print_board(board) if current_player human_player: # 人类回合 move_made False while not move_made: try: input_str input(f“玩家{current_player}请输入落子位置 (行,列): “) row_str, col_str input_str.split(‘,’) row, col int(row_str.strip()), int(col_str.strip()) if not (0 row BOARD_SIZE and 0 col BOARD_SIZE): print(f“坐标超出范围请输入0到{BOARD_SIZE-1}之间的数字。“) continue if make_move(board, row, col, current_player): move_made True # 检查胜负 if check_win(board, row, col, current_player): print_board(board) print(f“恭喜玩家 {current_player} 获胜“) return else: print(“该位置已有棋子请重新选择“) except (ValueError, IndexError): print(“输入格式错误请按 ‘行,列’ 的格式输入例如 ‘7,7’。“) else: # 电脑回合 print(“电脑思考中...“) move get_computer_move(board, computer_player) if move is None: print_board(board) print(“棋盘已满平局“) return row, col move make_move(board, row, col, computer_player) if check_win(board, row, col, computer_player): print_board(board) print(“电脑获胜“) return # 3. 切换玩家 current_player computer_player if current_player human_player else human_player if __name__ “__main__“: main()6.2 输入验证的强化上面的代码已经有了基本的输入验证异常捕获和范围检查。我们可以更进一步允许用户输入时带空格比如“7, 7”或“7 , 7”并且提供更明确的提示。# 在人类回合的输入处理部分可以这样优化 input_str input(...).replace(‘ ‘, ‘‘) # 移除所有空格 if ‘,‘ not in input_str: print(“输入格式错误请使用逗号分隔行和列。“) continue # 然后再 split(‘,’)6.3 平局判断的优化目前的平局判断依赖于get_random_move返回None这发生在棋盘完全下满时。但五子棋理论上在填满15x15225个格子前就可能无法再形成五连珠了虽然极罕见。一个更完善的平局判断可以放在每次落子后检查是否还有空位并且双方都无获胜可能这需要更复杂的逻辑。对于我们的简易版本棋盘下满即平局的逻辑已经足够。6.4 代码风格与可维护性使用常量像BOARD_SIZE,EMPTY ‘’,PLAYER_X ‘X’,PLAYER_O ‘O’这样的值都应该定义在文件开头的常量部分。这避免了“魔法数字”和“魔法字符串”让代码意图更清晰修改也更方便。函数文档字符串为每个函数添加简单的文档字符串“”“”“”说明其作用、参数和返回值。这对于你几个月后回来看代码或者别人阅读你的代码有巨大帮助。模块化我们把打印棋盘、落子、判断胜负、AI决策都分成了独立的函数。这使得代码结构清晰易于测试和调试。你可以单独测试check_win函数是否正确而不需要运行整个游戏。7. 运行效果与扩展思考当你运行完整的程序你会看到一个带坐标的棋盘你可以和电脑进行一轮轮的对战。电脑虽然笨只会随机下和找一步赢棋但作为第一个版本已经能带来不少乐趣了。这个项目到这里已经完成但它还有巨大的扩展空间这也是编程项目最吸引人的地方——你可以不断给它添加新功能增强AI现在的AI太弱了。你可以让它变得更聪明防守在找自己赢棋点之后加入检查人类玩家是否在下一步有赢棋点如果有就去堵住它。评估函数为每个空位打分。例如形成活四两头无阻挡的四子连线给极高分死四一头被堵给高分活三给中分等等。然后AI选择分数最高的点落子。这需要你设计一个评估棋形的函数。搜索算法实现极小化极大算法Minimax配合Alpha-Beta剪枝让AI能够向前看几步选择对自己最有利、对对手最不利的走法。这是经典棋类AI的标配挑战性很大但实现后成就感极高。图形化界面命令行棋盘毕竟不够美观。你可以使用Pygame、Tkinter或Pyglet等库为游戏创建一个真正的图形窗口用鼠标点击来下棋用图片代替X和O。网络对战使用socket编程让两个在不同电脑上运行的程序可以连接实现真人网络对战。这会涉及到网络通信、协议设计等新知识。游戏功能增加“悔棋”、“重新开始”、“选择棋盘大小”、“选择先手后手”等功能让游戏更完整。从我个人的经验来看把这个基础版本吃透比一开始就追求复杂功能更重要。理解每一行代码为什么这样写每个函数如何协作数据结构如何设计这些才是这个项目带给你的核心价值。当你扎实地掌握了这个基础版本再去尝试任何一项扩展都会发现思路清晰很多因为你是在一个稳固的地基上添砖加瓦而不是在沙地上盖楼。
返回列表