news 2026/10/1 17:00:32

低级别系统设计实战:Tic Tac Toe(井字棋)游戏的设计与多语言实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
低级别系统设计实战:Tic Tac Toe(井字棋)游戏的设计与多语言实现
  • 示例工程

【免费下载链接】awesome-low-level-design

Learn Low Level Design (LLD) and prepare for interviews using free resources.

项目地址:https://gitcode.com/GitHub_Trending/aw/awesome-low-level-design
点击查看免费下载

导读

本篇技术指南以awesome-low-level-design仓库中 problems/tic-tac-toe.md 为问题定义主体,系统讲解如何为经典的 3x3 井字棋游戏做面向面试的低级别系统设计(LLD)。文中完整继承需求文档中的 7 条功能需求与 4 个核心类(Player、Board、Game、TicTacToe),并深入仓库中 C++、Java 等多语言实现源码,剖析回合流转、落子校验、胜负判定、平局检测等关键逻辑,以及用 Strategy / State / Observer 模式、Minimax 算法扩展单机 AI 的工程化写法。读完本文,你将掌握从需求分析到类设计再到多语言落地的一整套 LLD 答题范式,可直接迁移到停车场、电梯、象棋等其他常见面试题。

一、需求分析:把一句话需求拆成可验收的规格

原文档 problems/tic-tac-toe.md 给出了 7 条需求,它们是整个设计的验收标准,任何实现都不能偏离:

  1. 游戏在 3x3 网格上进行;
  2. 两名玩家轮流在网格上放置自己的符号(X 或 O);
  3. 率先在横向、纵向或对角线上连成三个己方符号的玩家获胜;
  4. 网格全部填满且无人获胜时,游戏以平局结束;
  5. 游戏应提供用户界面来展示网格并允许玩家落子;
  6. 游戏应管理玩家回合并校验落子是否合法;
  7. 游戏应在结束时检测并宣布胜者或平局。

其中值得在面试中强调的两点:

  • 需求 3 与需求 4 是互斥的终止条件:要么出现胜者,要么棋盘填满,二者必居其一,这是游戏主循环跳出(game over)的判定依据;
  • 需求 6 里的“合法”包含两层校验:坐标必须落在 3x3 网格范围内,且该格子必须为空。仓库中 Board.cpp 的makeMove正是同时执行这两层校验后才落子。

Java 版的问题陈述 solutions/java/src/tictactoe/README.md 将棋盘抽象为 NxN、并补充了“可扩展性:易于修改棋盘尺寸或新增功能”这条需求,这提示我们:面试答题时应把需求归纳为「双人轮流落子」「胜负/平局检测」「输入校验」「可扩展」四个维度去组织类设计。

二、UML 类图:从需求到类的映射

原文档给出的 UML 类图位于 class-diagrams/tictactoe-class-diagram.png,其核心实体可以归纳为 4 个类,职责划分如下:

类职责关键方法
Player表示一名玩家,持有名字与符号(X/O)getName()、getSymbol()
Board表示 3x3 棋盘,提供落子、胜负检测、满盘检测makeMove()、checkWin()、isFull()、display()
Game管理游戏流程与玩家交互:回合流转、校验、判定结果play()、makeMove()、switchPlayer()、displayResult()
TicTacToe应用入口,创建玩家与游戏实例并启动main()

这个划分遵循典型的“数据(Board)+ 状态(Game)+ 实体(Player)+ 入口(TicTacToe)”分层:Board 只管网格数据与棋盘规则,不关心谁在下棋;Game 负责编排流程;入口类负责组装。面试时按“谁持有数据、谁驱动流程、谁组织启动”三步即可快速确定类边界。

三、核心类的职责与接口设计

3.1 Player:最小化的玩家实体

Player 只需三个字段:name、symbol('X' 或 'O')、以及标识是否人类玩家的isHuman(为 AI 扩展预留)。仓库 C++ 实现 Player.hpp 与 Player.cpp 通过构造函数默认参数bool isHuman = true提供了单机模式所需的扩展点——把isHuman设为false即得到一个由电脑控制的玩家。

3.2 Board:网格数据与棋盘规则

Board 在 Board.hpp 中声明的接口完整覆盖了需求中“落子、校验、判胜、满盘”四大能力:

  • isValidPosition(row, col):判断坐标是否在[0, size)范围内;
  • isEmpty(row, col):判断格子是否为空(空位用'-'字符表示);
  • makeMove(row, col, symbol):先校验再落子,失败返回false;
  • isFull():遍历全盘,存在空格即返回false;
  • checkWin(symbol):委托给checkRows / checkColumns / checkDiagonals三个私有方法;
  • display():以 3 列对齐方式打印当前棋盘,即需求 5 中的“用户界面”(控制台版);
  • reset():重置网格,便于复用棋盘实例。

3.3 Game:流程编排者

Game 在 Game.hpp 中维护board、两个玩家指针、currentPlayer与gameOver标志。核心循环play()的逻辑为:展示棋盘 → 若是人类玩家则读入 row/col 并调用makeMove,若是电脑玩家则调用computerMove→ 循环直到gameOver→ 调用displayResult()宣布胜者或平局。

四、胜负判定与平局检测:checkWin 的实现细节

Board 的胜负判定拆成三个方向分别检查,见 Board.cpp:

  • 行检查(checkRows):对每一行遍历所有列,若整行都是某符号则胜;
  • 列检查(checkColumns):对每一列遍历所有行,逻辑同构于行检查;
  • 对角线检查(checkDiagonals):主对角线grid[i][i],副对角线grid[i][size-1-i]。

checkWin用checkRows(symbol) || checkColumns(symbol) || checkDiagonals(symbol)组合三者。值得注意的工程细节是:仓库实现把循环写成通用size维度而非硬编码 3,这直接呼应了 Java 版 README 中“支持任意 NxN 棋盘”的可扩展性需求。

平局检测在 Game.cpp:落子合法后先查board.checkWin(currentPlayer->getSymbol()),若胜则gameOver = true;否则再查board.isFull(),若满盘则将currentPlayer置空——这一技巧让displayResult()只需判断currentPlayer是否为nullptr即可区分“某人获胜”与“平局”两种终局:

void Game::displayResult() const { board.display(); if (currentPlayer) { std::cout << currentPlayer->getName() << " wins!" << std::endl; } else { std::cout << "It's a draw!" << std::endl; } }

这种用「状态语义复用」代替「额外布尔标志」的做法是 LLD 面试中的加分点。

五、回合流转与落子校验:Game 的 makeMove 主链路

回合管理依赖currentPlayer指针的交替:

void Game::switchPlayer() { currentPlayer = (currentPlayer == player1) ? player2 : player1; }

makeMove的主链路(Game.cpp)是理解全流程的关键:

  1. board.isValidPosition(row, col)不通过 → 打印 "Invalid position!" 并直接返回(不切换回合);
  2. board.isEmpty(row, col)不通过 → 打印 "Position already taken!" 并返回(不切换回合);
  3. board.makeMove落子;
  4. checkWin成立 → 结束;
  5. isFull成立 → 结束(平局);
  6. 否则switchPlayer()轮到下一位。

注意第 1、2 步失败时都不切换玩家,这保证了“非法落子不消耗回合”的规则正确性,与需求 6 “校验落子确保合法”严格一致。

六、从 3x3 到 NxN:尺寸无关的通用化设计

原需求只要求 3x3,但仓库两种实现都做了通用化处理:

  • C++ 版:Board构造函数带默认参数Board(int size = 3)(见 Board.hpp),所有判定循环都以size为界;
  • Java 版:Game.java 在构造时new Board(3)显式指定尺寸,而 solutions/java/src/tictactoe/README.md 将“支持不同棋盘尺寸”列为扩展方向。

这意味着若面试官追问“棋盘改为 4x4、5x5 怎么办”,上述实现只需把落子判定从“三连”推广为“size连”即可,类结构无需任何改动——这正是“把维度参数化”设计带来的扩展收益。

七、工程化扩展:设计模式与 AI 对弈

原文档只给出 4 个基础类,仓库实现则在基础骨架之上展示了更强的工程化扩展,可作为面试时的进阶谈资。

7.1 Java 版:Strategy + State + Observer 三模式组合

Java 实现 solutions/java/src/tictactoe/ 采用了三层设计模式:

  • Strategy 模式:把“行胜/列胜/对角胜”拆成三个独立的WinningStrategy实现(strategy/RowWinningStrategy.java、strategy/ColumnWinningStrategy.java、strategy/DiagonalWinningStrategy.java),Game持有策略列表,checkWinner遍历调用(见 Game.java)。新增“四连”“斜向更长连法”只需加策略类,符合开闭原则;
  • State 模式:把游戏生命周期建模为InProgressState、WinnerState、DrawState三个状态(state/),Game.makeMove委托给当前状态的handleMove,终局判定被状态机化;
  • Observer 模式:Game继承GameSubject,当状态变为非 IN_PROGRESS 时通知Scoreboard等观察者(Game.java),为后续接 UI、记分板等外部系统提供了松耦合出口。

7.2 C++ 版:Minimax 算法驱动的电脑玩家

C++ 实现把需求“双人”扩展为“人机对战”:Game::initializePlayers("Human", "Computer")中第二个玩家以isHuman=false创建(TicTacToeDemo.cpp)。其 AI 核心是经典的Minimax 极大极小搜索(Game.cpp):

  • 评分规则:电脑胜得10 - depth,人类胜得depth - 10,满盘平局为 0(depth越小越优先,追求速胜、避免拖局);
  • isMax层取最大值(电脑视角),isMin层取最小值(人类视角);
  • 搜索通过拷贝棋盘Board tempBoard = board进行假设性落子,不污染真实棋盘;
  • findBestMove()遍历每个空位,模拟落子后用minimax求分,取最高分位置落子(Game.cpp)。

对 3x3 棋盘,Minimax 全搜索即可保证电脑不输(最佳应对),是面试中“如何让机器不犯错”的标准答案。可以顺便指出:更大的棋盘(如 5x5)需要 Alpha-Beta 剪枝或蒙特卡洛方法,这能体现你对算法复杂度的意识。

八、多语言实现导航与运行方式

该题在仓库中提供了 5 种语言的完整实现,可直接对照阅读同一份设计的差异化落地:

语言位置入口/说明
Javasolutions/java/src/tictactoe/TicTacToeDemo.java演示,Game/Board/Player/Cell分包组织,README 含类设计与示例代码
C++solutions/cpp/tictactoe/TicTacToeDemo.cpp为入口,含人机对战与 Minimax AI
C#solutions/csharp/tictactoe/与 C++ 同套类设计的 .NET 版本
Gosolutions/golang/tictactoe/以 Go 结构体与接口组织的版本
Pythonsolutions/python/tictactoe/Python 面向对象版本

C++ 版可直接编译运行体验完整人机对战流程:

# 在解决方案目录下编译并运行(需 C++17 以支持结构化绑定) g++ -std=c++17 TicTacToeDemo.cpp Board.cpp Game.cpp Player.cpp -o tictactoe ./tictactoe

运行后程序会打印棋盘(空位显示-),提示人类玩家输入row与column(取值 0~2),电脑玩家则由 Minimax 自动落子,最终输出胜者或It's a draw!。Java 版则参考其 README 中的示例用法:

Player p1 = new Player("Alice", Symbol.X); Player p2 = new Player("Bob", Symbol.O); Game game = new Game(p1, p2); game.play();

九、面试答题要点总结

结合需求文档与仓库实现,回答本题时的关键得分点归纳如下:

  1. 先定验收标准再画类图:7 条需求逐条映射到类的哪个方法,做到“需求可追踪”;
  2. 明确职责边界:Board 管数据与规则,Game 管流程,Player 是纯实体,入口类只做组装;
  3. 讲清两种终局:checkWin 三方向判定 + isFull 满盘平局,注意“非法落子不换回合”的细节;
  4. 主动展示扩展性:棋盘尺寸参数化(NxN)、Java 的 Strategy/State/Observer 组合、C++ 的 Minimax 人机对战,都是把“能跑”升级为“好扩展”的证明;
  5. 多语言对照:能指出 5 种语言的实现位置并对比其组织方式,说明你不仅会设计,还能工程化落地。

这套“需求 → 类职责划分 → 核心算法 → 设计模式扩展 → 多语言落地”的流程,可以直接套用到仓库中 problems/ 下的停车场、电梯、象棋、餐厅管理等同类 LLD 题目,是面试准备的通用方法论。

  • 示例工程

【免费下载链接】awesome-low-level-design

Learn Low Level Design (LLD) and prepare for interviews using free resources.

项目地址:https://gitcode.com/GitHub_Trending/aw/awesome-low-level-design
点击查看免费下载

相关推荐

上一篇:终极指南:如何在Mac上免费读写NTFS移动硬盘
下一篇:N_m3u8DL-RE 流媒体下载指南:M3U8、DASH 与直播录制一次跑通

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 16:57:54

RPG Maker Unite 安装全流程:从 Epic 到 Unity Hub 完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 16:57:40

ruoyi-vue-pro集成积木报表:启用报表设计器与模块落地方案

第一次把 ruoyi-vue-pro 后台的报表模块跑通时&#xff0c;我没少走弯路。侧边栏看不到“报表设计器”&#xff0c;后端日志里连一条报错都没有&#xff0c;查了半天才发现是三个环节没对齐&#xff1a;后端依赖没引入、数据库里缺积木报表的核心表、system_menu里的菜单权限没…

作者头像 李华
网站建设 2026/10/1 16:55:12

Ubuntu下Realtek 8812BU USB网卡驱动安装与排查指南

一块Realtek 8812BU USB网卡&#xff0c;Windows下插上就能用&#xff0c;换到Ubuntu上之后&#xff0c;要么插上去一点反应都没有&#xff0c;要么lsusb能看到设备&#xff0c;但右上角的网络菜单里死活找不到Wi-Fi开关。这种问题我前前后后在四五台机器上碰到过&#xff0c;每…

作者头像 李华