简介一份基于Qt/C的黑白棋完整项目代码适合正在学习Qt界面开发、C游戏逻辑或网络编程的开发者参考。项目中棋盘界面由Qt Designer设计包含控件布局与交互响应核心规则模块负责合法落子判断、翻转规则、计分和胜负检测并带有双人网络对战思路可作为课程设计或毕业设计的起点。资源共15个文件压缩包仅1.12MB以cpp/h源码为主另有ui界面文件、pro工程配置、规则c文件以及jpg/png图片素材工程结构紧凑便于直接编译阅读和二次开发。已有348人学习/下载。通过该工程能够了解界面代码的自动生成与调用方式掌握事件处理流程并初步认识QTcpSocket/QUdpSocket在双人实时对战中的应用同时涉及消息同步、心跳包、超时重传等网络编程细节对提升C工程组织能力和算法设计能力有明显帮助。1. 项目概述与核心思路黑白棋也叫奥赛罗棋Othello是一个特别适合拿来做编程练手的棋类游戏。它规则不复杂但算法里牵扯到的逻辑判断、边界处理、策略搜索覆盖面相当广。拿它来写代码你能把二维数组操作、方向遍历、状态判定、甚至是简单AI算法全部串起来练完这一套再去写别的棋牌类项目、寻路算法、甚至是状态机逻辑都会顺很多。先说清楚这篇博文要做什么我会从零开始用Python实现一个完整的黑白棋游戏包含标准8x8棋盘、双方轮流落子、合法落子判定、翻转棋子、跳过回合、终局胜负统计。代码部分可以直接跑起来同时我会把每个关键函数的判断逻辑拆开讲清楚——尤其是“怎么判断一个落子点是否合法”和“怎么把该翻转的棋子全部翻过来”这两个核心问题很多人第一次写黑白棋坑都踩在这两个地方。适合谁来读如果你刚学完Python基础语法想找一个不靠抄、能自己写出来的小项目或者你已经在写棋类程序想看看别人的判断逻辑是怎么组织的又或者你想在黑白棋基础上做一个人机对战AI那这篇博文都适合你。我会把每一步怎么想、为什么这么写都交代明白而不是直接甩一份代码让你自己琢磨。1.1 黑白棋规则里最容易忽略的三个细节在动笔写代码之前规则必须吃透。黑白棋的基本规则棋盘开局时正中间四颗子交叉摆放黑棋先手双方轮流在棋盘上落子落子的位置必须能“夹住”至少一个对方棋子落子后所有被夹住的对方棋子翻转为己方棋子。当双方都无法落子时游戏结束棋子多的一方获胜。听起来简单但有三个细节是新手特别容易忽略的。第一个细节“夹住”是可以在八个方向上同时发生的。也就是说你落一颗子可能同时夹住横向、纵向、斜向的多条线每条线上被夹住的棋子都要翻。代码里要同时检查八个方向而不是只检查一个。第二个细节被夹住的棋子必须在同一条直线上中间不能有空格或者己方棋子隔断。换句话说从你落子的位置出发沿着一个方向走到对方棋子之后必须再遇到一颗己方棋子才能构成合法翻转。如果走出去只碰到边界或者空格这一条方向就不合法。第三个细节如果一个玩家没有合法落子位置必须自动跳过回合由对方继续落子。这个规则在代码里对应“当前玩家无棋可下”的状态判定很多人会漏掉结果程序直接卡死。规则搞清楚了下面进入正题。1.2 为什么拿Python实现黑白棋选Python写黑白棋不是因为Python性能有多强而是因为它处理二维逻辑和快速原型非常顺手。你不需要关心内存布局不需要手动管理棋盘数组的边界用双层列表天然就能表示8x8棋盘配合列表推导式、枚举函数这些东西代码写出来非常直观。而且后续如果你想扩展AI功能Python生态里现成的算法模板、可视化库也很多比如你想把人机对战做成图形界面用pygame或者tkinter都行想加AI决策minimax、alpha-beta剪枝这类经典算法Python写起来也最顺手。说实话黑白棋这种项目用Python做逻辑量和代码量都刚好卡在学习者的舒适区——不会简单到无话可说也不会复杂到劝退。我自己的经验是完整写过一次黑白棋之后你对“状态”这个词的理解会完全不一样。2. 核心数据结构与算法拆解2.1 棋盘建模用两层列表还是数字矩阵棋盘建模是一切的起点。黑白棋棋盘是8x8的二维网格最自然的表示方式就是嵌套列表也就是一个大列表里装着8个小列表每个小列表代表一行每个元素代表一个格子。我建议这样定义棋子状态0空格1黑棋2白棋为什么不直接用字符串或者布尔值因为后续做AI估值的时候你往往需要把棋盘状态编码成数字输入给算法用数字是最省事的。而且数字的打印和调试也方便一眼就能看出棋盘上每一处的状态。初始化棋盘的时候中间四颗子的位置是固定的。标准的开局是第3行第3列黑棋第3行第4列白棋第4行第3列白棋第4行第4列黑棋。这里说的行和列都是0-based索引也就是从0数到7中间四个点分别是(3,3)、(3,4)、(4,3)、(4,4)。还有一个小细节有些实现会把棋盘定义成10x10或者带边框的矩阵目的是简化边界判断——反正越界的位置都是无效位置直接跳过就行。但我觉得8x8内加边界判断更干净因为边界判断的代码只在一个集中函数里出现不会到处散落。2.2 方向向量的使用八个方向的统一处理写棋盘类程序有一个很实用的技巧把方向统一成“向量”。在8x8棋盘上一个位置可以沿八个方向移动对应八个坐标变化量水平左右(0, 1)、(0, -1)垂直上下(1, 0)、(-1, 0)两条对角线(1, 1)、(1, -1)、(-1, 1)、(-1, -1)把方向定义成这样的元组列表之后写合法性判断的时候就可以用一个循环把八个方向逐个检查而不是写八个重复代码块。这个思想在后续很多棋盘类游戏里都用得到比如五子棋的胜利判断、国际象棋的走法生成本质都是同一套路。# 八个方向 DIRS [ (0, 1), (0, -1), (1, 0), (-1, 0), (1, 1), (1, -1), (-1, 1), (-1, -1) ]2.3 落子合法性判断从落子点朝八个方向“吃”棋子这是整个黑白棋最核心的算法没有之一。判断逻辑可以分成两层首先是“有没有路”。从当前落子位置(x, y)出发沿着某个方向(dx, dy)一步步往外走每走一步要满足两个条件不出界、而且落在对方棋子上。如果第一步走出去是空格那这个方向直接废掉如果第一步走出去就是己方棋子那这个方向也是废的因为中间根本没夹住任何对方棋子。其次是“有没有终点”。沿着某个方向走的过程中如果遇到了己方棋子那么说明从落子点到这颗己方棋子之间的所有对方棋子都可以被翻转这就是“夹住”的意思。如果一直走到棋盘边界都是对方棋子或者空格没有遇到己方棋子这个方向同样不合法。所以合法性判断的本质是存在至少一个方向从落子点出发第一步是对方棋子继续走能遇到己方棋子。我贴一下具体的判断函数代码里有详细的注释def is_valid_move(board, row, col, player): 判断 (row, col) 是否是 player 的合法落子点 player: 1 黑棋 / 2 白棋 # 落子位置必须是空格 if board[row][col] ! 0: return False opponent 3 - player # 对方棋子 for dr, dc in DIRS: r, c row dr, col dc # 第一步必须落在棋盘内而且是对方棋子 if not (0 r 8 and 0 c 8) or board[r][c] ! opponent: continue # 沿着这个方向继续走 r dr c dc while 0 r 8 and 0 c 8: if board[r][c] player: return True if board[r][c] 0: break r dr c dc return False初学者最容易搞错的一个地方直接从落子点往外找己方棋子而忽略了“第一步必须是对方棋子”。不加这一步判断的话会出现一个离谱的局面——落子点的斜对角就是己方棋子中间空一格也被判定成合法落子这显然是错的。2.4 翻转棋子的实现返回而非修改是更好的设计判断合法之后下一步就是翻转。这里有一个设计上的小讲究我见过很多初版代码一上来就写一个flip(board, row, col, player)函数直接原地修改棋盘。这个做法在单次落子场景下没问题但当你后面做人机对战时AI需要反复模拟落子、撤销落子来评估局面。如果翻转函数直接改原棋盘你还要额外写一个复制棋盘的逻辑否则模拟完一次局面就乱了。我的建议是翻转函数只负责返回“需要翻转的棋子位置列表”对外层棋盘的操作由业务逻辑去做。这样做的好处是函数是纯函数输入棋盘和落子位置输出翻转列表不产生副作用测试起来也方便。def get_flips(board, row, col, player): 返回 (row, col) 落子后所有需要翻转的棋子位置列表 flips [] opponent 3 - player for dr, dc in DIRS: path [] r, c row dr, col dc while 0 r 8 and 0 c 8 and board[r][c] opponent: path.append((r, c)) r dr c dc # 走出循环后如果当前位置是己方棋子说明 path 里所有棋子可翻转 if 0 r 8 and 0 c 8 and board[r][c] player: flips.extend(path) return flips这里有个容易踩的坑get_flips和is_valid_move的判断逻辑非常相似能不能复用从思路上说可以——is_valid_move本质上就是len(get_flips(board, row, col, player)) 0。我在实际代码里通常会把两个函数分开原因是is_valid_move在局面的合法性筛选时被高频调用性能上更轻量而get_flips只在确认落子时才调用。两者语义轻重不同分着写反而更清晰。3. 实操过程完整代码实现3.1 棋盘初始化和显示这部分最简单但我还是要提一个工作习惯把棋盘的初始化、当前的玩家、双方棋子数量全部封装成全局状态方便后续扩展。就算你现在只做双人对战也要养成“状态集中管理”的意识后面加AI或者加入悔棋功能时你会发现这个决策能帮你省掉大把时间。def init_board(): 初始化8x8棋盘中间四子按标准放置 board [[0] * 8 for _ in range(8)] board[3][3] 1 board[4][4] 1 board[3][4] 2 board[4][3] 2 return board def print_board(board): 打印棋盘 print( .join(str(i) for i in range(8))) for i in range(8): row [str(i)] for j in range(8): if board[i][j] 0: row.append(.) elif board[i][j] 1: row.append(●) else: row.append(○) print( .join(row))print_board里我用.表示空格用实心圆和空心圆表示黑白棋。你也可以用B和W但打印出来视觉效果差一些。这里有一个小技巧给玩家编号1和2是黑棋和白棋分别对应不同的字符后续所有逻辑都围绕数字展开展示层只做转换这样职责分离。3.2 获取所有合法落子点在实际对局中玩家需要的不是一个一个试哪个位置能下而是希望程序把所有可落子位置标出来。这个逻辑特别简单就是在整个棋盘上遍历所有空格逐个用is_valid_move判断。def get_valid_moves(board, player): 返回 player 当前所有合法落子位置 moves [] for r in range(8): for c in range(8): if board[r][c] 0 and is_valid_move(board, r, c, player): moves.append((r, c)) return moves注意这个函数会在每一回合都调用一次。如果返回的列表为空说明当前玩家无棋可下必须跳过。这一块的逻辑必须在主循环里处理否则很容易出现玩家输错坐标或者程序报错退出的情况。3.3 落子与主循环落子逻辑串起来就是三件事验证位置合法、翻转棋子、把棋子放到落子点上。顺序上要注意翻转操作是基于当前棋盘状态的所以要先把翻转位置拿到再统一更新棋盘。如果把棋子先放上去再取翻转列表翻转逻辑就会出错——因为己方棋子已经占据了这个位置方向上判断会出问题。def make_move(board, row, col, player): 执行落子返回是否成功 if not is_valid_move(board, row, col, player): return False flips get_flips(board, row, col, player) board[row][col] player for r, c in flips: board[r][c] player return True主循环的完整结构如下。这里我额外处理了两个点一是跳过回合时保留原玩家继续下二是终局时游戏结束标志的触发条件。def count_pieces(board): 统计黑白双方棋子数量 black sum(row.count(1) for row in board) white sum(row.count(2) for row in board) return black, white def main(): board init_board() current_player 1 # 黑棋先手 while True: print_board(board) black, white count_pieces(board) print(f黑棋: {black} 白棋: {white}) valid_moves get_valid_moves(board, current_player) # 当前玩家无棋可下 if not valid_moves: other_player 3 - current_player other_moves get_valid_moves(board, other_player) if not other_moves: # 双方都不能下游戏结束 break print(f玩家 {current_player} 无合法落子跳过回合) current_player other_player continue print(f当前玩家: {黑棋 if current_player 1 else 白棋}) print(f合法落子位置: {valid_moves}) try: row int(input(请输入行号(0-7): )) col int(input(请输入列号(0-7): )) except ValueError: print(输入无效请输入数字) continue if (row, col) not in valid_moves: print(该位置不是合法落子点请重新输入) continue make_move(board, row, col, current_player) current_player 3 - current_player print_board(board) black, white count_pieces(board) if black white: print(f黑棋获胜比分 {black}:{white}) elif white black: print(f白棋获胜比分 {black}:{white}) else: print(f平局比分 {black}:{white}) if __name__ __main__: main()这里3 - current_player是一个很巧妙的交换写法当current_player为1时3-12当current_player为2时3-21。两行代码就能实现玩家切换不用写if判断。3.4 我实际调试中踩过的三个坑第一个坑valid_moves判断没有提前缓存。刚开始写的时候我在玩家输入坐标之后直接调is_valid_move(board, row, col, player)判断但玩家在输入之前界面里显示的是遍历出来的合法点输入之后又重新算一遍。这个逻辑上没问题但调试时很容易出现“用户看到的合法点”和“程序实际判断的合法点”不一致的情况。所以我把合法点存在一个变量里显示和判断都用同一个列表彻底杜绝了这个隐患。第二个坑没有处理无合法落子时的循环。有一次测试时程序走到终局前某一方已经没有合法落子了但因为我没有做跳过处理主循环直接死循环了。后来加了跳过逻辑但最初我只跳了一次没想到双方都可能连续无棋可下所以在判断当前玩家无棋可下之后必须再判断对方有没有棋可下才能决定是否结束。第三个坑边界条件没有测试完整。棋盘四个角落的落子、边线上的落子判断逻辑最容易出错。我的建议是写几个已知棋谱局面来验证代码正确性比如黑白棋里著名的“翼型开局”局面黑棋第一步下到(2, 3)或(3, 2)逐步验证每一步的合法落子是否符合标准棋谱。4. 常见问题与排查技巧实录写黑白棋代码的过程中有几个问题出现的频率特别高我把它们整理成一张速查表遇到问题直接对号入座。4.1 常见错误速查表错误现象可能原因排查方法落子后棋子没有翻转get_flips中第一步没有判断对方棋子打印落子点每个方向的path变量合法落子点判定太多方向判断没有要求“最终遇到己方棋子”检查is_valid_move中while循环跳出条件玩家输入坐标后程序崩溃输入了非数字字符输入代码外层加try-except某一方无棋可下但程序没有跳过主循环里没有检测valid_moves为空的情况在每回合开始时检查valid_moves空列表终局比分错误翻转逻辑重复翻转或者落子前统计了棋子数在make_move前后分别打印棋盘对比状态棋盘打印偏移列标题用join时字符宽度不一致统一使用单个数字和符号注意中文字符宽度4.2 调试技巧单步打印在黑白棋里特别好用黑白棋这种棋盘类程序的调试最有效的方式不是断点而是在关键判断函数里临时加打印语句。我调试is_valid_move的时候会在每个方向判断处打印当前的行列值和棋盘状态这样一眼就能看出来是哪个方向判断出错。有一个我自己总结的小方法在调试时把棋盘打印成数字模式用0、1、2标识状态避免字符显示带来的干扰。因为●和○在终端里显示的宽度和数字不一样视觉上容易看歪。def debug_board(board): 打印数字版棋盘方便调试 for row in board: print( .join(str(x) for x in row))当你确认逻辑正确之后再切换回字符样式。这个习惯能让你更快定位是显示问题还是逻辑问题。4.3 性能问题黑白棋需要做性能优化吗很多人写完第一版之后会问这个程序能跑多快说实话纯双人对战的场景性能完全不是问题——8x8的棋盘总共64个格子每回合最多遍历64个位置每个位置检查8个方向单次计算量撑死几百次操作人类玩家下棋程序性能连1%都用不到。真正的性能瓶颈出现在后面如果你要写AIAI需要考虑很多步之后的局面每一步都要模拟落子、翻转、评估、回溯。这时候同样的判断函数会被调用成千上万次性能就变得重要了。到时候可以做的优化有把棋盘编码成位棋盘bitboard用位运算处理这个方案在黑白棋里尤其有效因为64个格子正好对应64位整数的每一位或者用对称性剪枝减少搜索空间。但这些是进阶内容等把基础版跑通再做也不迟。5. 扩展方向从双人对战到AI助手5.1 一个最简单的AI策略贪心算法写完了双人对战版本下一步最自然的扩展就是加入AI。最简单的AI策略是贪心每次选择能让己方棋子数量增加最多的落子点。听起来很幼稚但实测下来贪心AI对完全不会玩的新手还是有一定威胁的尤其是面临终局的时候棋子数量最大化往往能赢下一些局。实现起来很简单只需要在AI回合遍历所有合法落子点用get_flips统计每个点能翻转多少个棋子选翻转数量最多的那个即可。def ai_move_greedy(board, player): 贪心AI选择翻转棋子数最多的落子点 best_move None best_count -1 for row, col in get_valid_moves(board, player): flips get_flips(board, row, col, player) if len(flips) best_count: best_count len(flips) best_move (row, col) return best_move不过贪心AI有一个致命弱点它只顾眼前的翻转数量不考虑后续局面的好坏。黑白棋是一个“少即是多”的游戏——角上的棋子是永久性的因为角不会被翻转而边上的棋子相比之下还算稳定。贪婪拿中间位置虽然一时棋子多但可能把角的控制权送给了对手。如果你想让AI更聪明可以引入“位置权重表”给角、边、中心位置不同的分数让AI在每个位置计算一个“落子后局面评分”这比纯贪心要强很多。5.2 加入AI之后的主循环改动加了AI之后主循环的改动其实很小。只需要判断当前玩家是人类还是AI是AI就调用AI函数自动落子是人工就继续走输入流程。这也验证了前面设计的好处因为状态都集中管理AI回合和人类回合的调用接口一样都是“给定棋盘和玩家算出落子位置”至于这个位置是用户输入的还是算法算出来的主循环根本不需要关心。is_ai {1: False, 2: True} # 黑棋人工白棋AI if is_ai[current_player]: row, col ai_move_greedy(board, current_player) print(fAI ({白棋 if current_player 2 else 黑棋}) 落子: ({row}, {col})) else: # 人类输入逻辑不变 ...这里有一个小细节值得注意AI落子之前最好加一个小延迟比如time.sleep(1)否则AI秒落子你根本看不清它的走法对局的沉浸感会大打折扣。5.3 想做可视化界面pygame的基本思路如果不想在命令行里玩可以用pygame做一个简单的图形界面。核心思路把8x8棋盘渲染成网格每格根据状态绘制黑色圆或白色圆玩家点击棋盘时把鼠标坐标换算成行列坐标再走相同的合法性判断逻辑。import pygame # 假设棋盘渲染相关常量 CELL_SIZE 60 BOARD_SIZE 8 def mouse_to_board(pos): 把鼠标坐标换算成棋盘行列坐标 x, y pos col x // CELL_SIZE row y // CELL_SIZE if 0 row BOARD_SIZE and 0 col BOARD_SIZE: return row, col return None, None这种UI渲染代码本身不难难点还是在游戏逻辑。所以我的建议是先把命令行版本跑通、跑对再考虑加UI。逻辑和显示分离的结构确实让我在后续加pygame界面的时候省了很多事基本没改逻辑代码只加了渲染层。最后再分享一点个人经验黑白棋代码这个项目我前前后后写过三遍。第一遍在大学选课时用C语言写了一个非常粗糙的版本函数满天飞判断逻辑一团乱麻跑起来经常出bug。第二遍是求职前用Python重写精简了结构把核心算法提炼成短小精悍的函数面试时正好被问到当场写了一遍合法性判断逻辑清晰面试官印象还挺好。第三遍就是这篇博文呈现的版本加入了AI和可视化扩展。如果你正在学编程我强烈建议找个机会把黑白棋完整写一遍。不是因为黑白棋本身多有用而是它几乎覆盖了所有基础编程的核心能力二维数组操作、状态管理、复杂条件判断、模块化设计、算法优化思路、甚至人机交互逻辑。这些能力放到任何项目里都是通用的。最后送你一个查bug的小技巧遇到棋盘类程序出错先别急着看代码手工在草稿纸上画出当前棋盘状态和你想测试的落子点沿着八个方向一步步模拟一遍棋子的翻转过程再对照你的代码逻辑走一遍绝大多数的非法判定和漏翻转问题都能当场发现。这个习惯我到现在还在用真的很管用。本文还有配套的精品资源点击获取