FEATURED · 精选文章

基于 Ruby 面向对象编程:命令行井字棋(Tic Tac Toe)项目实战指南

发布时间 / 2026/9/16 14:45:34
来源 / 创域科博编辑部
栏目 / 资讯中心
基于 Ruby 面向对象编程:命令行井字棋(Tic Tac Toe)项目实战指南 基于 Ruby 面向对象编程命令行井字棋Tic Tac Toe项目实战指南【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum导读本篇技术指南围绕本项目The Odin Project 开源课程仓库中 Ruby 面向对象编程基础章节的井字棋项目作业展开目标是引导你用刚学到的 OOP面向对象编程能力在命令行中构建一个双人互搏、每回合刷新棋盘显示的井字棋游戏。读完本文你将掌握如何拆分类 / 实例变量 / 方法、如何通过类的职责划分与信息隔离组织游戏逻辑以及如何用require_relative组织多文件 Ruby 项目为后续 Mastermind、Hangman 乃至 Chess 等更复杂的 OOP 项目打下基础。为什么是井字棋最适合练习 OOP 的“小问题”井字棋Tic Tac Toe又名 Noughts and Crosses规则极其简单两名玩家轮流在 3×3 棋盘上落子率先在横、竖、斜任一方向连成三子者获胜。正如课程在 Ruby 课程总览中所说通过这门课程你将亲手构建Tic Tac Toe、Hangman、Chess等项目把“意大利面条式的代码”拆分成清晰独立的类。井字棋之所以被选为 OOP 章节的第一个项目是因为它天然蕴含了 OOP 的经典要素若干参与者两个玩家Player一个共享状态棋盘Board不断重复的游戏循环轮流落子 → 刷新显示 → 判定胜负game loop。“几名玩家、一块棋盘、在游戏循环中检查胜利”——这些条件凑在一起恰好构成了一个可以用类class、实例变量instance variable和方法method来优雅建模的小型问题。正如 面向对象编程课程 强调的DRY不要重复自己、模块化、让类和方法只做一件事、尽量少地向外界暴露接口、不要让方法或类彼此重度依赖——这些原则在这个项目里都会得到充分练习。项目任务要求本项目位于 ruby/object_oriented_programming_basics/project_tic_tac_toe.md核心任务如下在命令行构建一个井字棋游戏两名人类玩家可以互相对战并且棋盘要在每回合之间显示出来。具体的作业步骤是先想后写思考你会如何搭建游戏中的不同元素……什么是类什么是实例变量什么是方法花几分钟思考可以避免浪费一小时的编码时间。动手构建构建你的游戏注意不要在不必要的情况下在类之间共享信息。提交与对照提交你的解决方案然后对照参考实现检查自己的设计。这套“先设计 → 再实现 → 再对照”的流程与同章节 Mastermind 项目的“先想清楚怎么搭再写代码”的要求一脉相承目的就是强迫你在写代码前先做对象建模。动手前的设计思考什么是类、实例变量、方法原文档要求你在动手前思考“什么是类什么是实例变量什么是方法”。这是整个项目成败的关键一步。针对井字棋我们逐一分析类的划分井字棋世界里的核心“名词”就是天然的对象候选候选类职责说明Board棋盘存储 3×3 的格子状态、渲染棋盘、判断是否满盘游戏的“共享状态”所在Player玩家持有玩家标记X / O与姓名极简类主要承载数据Game游戏控制回合流转、接收落子输入、调用胜负判定游戏循环的主控者判断一个“名词”是否值得成为类可以参考课程 managing_ruby_projects.md 的约定每个类一个文件。如果某个概念有自己独立的职责和数据就值得拆成一个类如果它只是别的方法里的一个临时值就应该是一个局部变量。实例变量的划分实例变量variable承载的是对象的持久状态需要区分“谁拥有这份数据”棋盘上的格子状态数组grid或board属于Board玩家使用的标记符号markerX/O属于Player当前轮到谁、游戏是否结束这类流程状态属于Game。判断标准很简单这份数据会被谁反复读取和修改就把它放进谁里。棋盘格子只被棋盘渲染和胜负判定读取所以它属于Board而“轮到谁”由游戏循环控制所以它属于Game。方法的划分方法的本质是“行为”要遵循课程强调的单一职责Board#display把棋盘打印到终端Board#update/Board#place_marker在某格落子Board#full?判断棋盘是否已满平局条件Game#win?/Board#winning_line?判断是否存在三连Game#play启动并驱动整个游戏循环。关于胜负判定逻辑放在Board还是Game两种设计都合理如果你认为“某标记是否形成三连”是棋盘自身的属性棋盘知道自己上面发生了什么就放进Board如果你认为它是“游戏规则”的一部分就放进Game。关键是选定一处并保持一致避免同一逻辑在两个类里重复出现。参考实现骨架类与方法的组织方式结合上述设计下面给出一份可运行的参考骨架。它遵循“先想后写”的原则把棋盘、玩家、游戏三个关注点分离。首先是棋盘类# lib/board.rb class Board WINNING_LINES [ [0, 1, 2], [3, 4, 5], [6, 7, 8], # 横 [0, 3, 6], [1, 4, 7], [2, 5, 8], # 竖 [0, 4, 8], [2, 4, 6] # 斜 ].freeze def initialize grid Array.new(9, ) end def display puts #{grid[0]} | #{grid[1]} | #{grid[2]} puts --------- puts #{grid[3]} | #{grid[4]} | #{grid[5]} puts --------- puts #{grid[6]} | #{grid[7]} | #{grid[8]} end def place_marker(position, marker) return false unless valid_move?(position) grid[position] marker true end def valid_move?(position) position.between?(0, 8) grid[position] end def full? grid.none? { |cell| cell } end def winner?(marker) WINNING_LINES.any? { |line| line.all? { |index| grid[index] marker } } end end注意WINNING_LINES被定义为常量并freeze因为它是所有棋盘实例共享、且永不改变的规则数据而grid是实例变量因为每局游戏的棋盘状态都不同。这与课程在 object_oriented_programming.md 中讲解的“实例变量与类变量的区别”知识点直接呼应。玩家类只需要极简地持有标记与姓名# lib/player.rb class Player attr_reader :name, :marker def initialize(name, marker) name name marker marker end end游戏类负责“编排”它持有两个玩家和一块棋盘通过循环驱动回合# lib/game.rb class Game def initialize(player1, player2) board Board.new players [player1, player2] current_player players.first end def play loop do board.display position ask_for_move board.place_marker(position, current_player.marker) break if game_over? switch_turns end announce_result end private def ask_for_move print #{current_player.name}#{current_player.marker}请选择空格编号 0-8 gets.chomp.to_i end def switch_turns current_player current_player players.first ? players.last : players.first end def game_over? board.full? || board.winner?(current_player.marker) end def announce_result board.display if board.winner?(current_player.marker) puts #{current_player.name} 获胜 else puts 平局 end end end最后是入口文件只负责装配并启动# main.rb require_relative lib/board require_relative lib/player require_relative lib/game player1 Player.new(玩家一, X) player2 Player.new(玩家二, O) Game.new(player1, player2).play运行方式在项目根目录的终端中ruby main.rb类间信息隔离不共享比共享更重要原文档第二条要求是“注意不要在不必要的情况下在类之间共享信息taking care to not share information between classes any more than you have to”。这正是 面向对象编程课程 中“尽量少地向世界展示你的接口”“不要让方法或类重度依赖彼此”的具体实践。对照上面的骨架可以总结出三条“信息隔离”守则通过方法而非裸数据交互Game不直接读写board的内部数组而是调用Board#place_marker、Board#winner?、Board#full?。这样即使把grid的实现从数组改成哈希Game也无需改动。让数据待在“主人”那里格子状态归Board、标记归Player、流程状态归Game。不要把棋盘数组塞进Game也不要在Player里保存棋盘——谁拥有这份状态谁才应该读写它。用只读访问器控制暴露面Player只通过attr_reader暴露name和marker没有任何可写接口外部无法篡改玩家身份。如果某份数据对外只读就只提供 reader。一个常见的反面示例是让Game直接操作一个全局数组、或者在Player里内置对Board的依赖。这类写法虽然能跑通但在代码量增长后例如扩展到 Mastermind、Connect Four会迅速变得难以维护。本项目正是为“类之间保持低耦合”建立肌肉记忆的关键训练。多文件组织用 require_relative 组装项目课程在 managing_ruby_projects.md 中强调了两条 Ruby 项目组织惯例每个类一个文件把所有 Ruby 源文件放进lib目录根目录保留一个入口文件如main.rb。井字棋项目的推荐目录结构如下tic_tac_toe ├── lib │ ├── board.rb │ ├── player.rb │ └── game.rb └── main.rb跨文件引用自己写的代码时使用require_relative。它的语义是相对于当前文件所在目录查找被引用的文件.rb后缀可省略与你在哪个目录下执行命令无关。因此main.rb里写require_relative lib/boardlib/game.rb里写require_relative player、require_relative board因为game.rb本身就在lib目录中即可。# lib/game.rb require_relative board require_relative player需要特别留意的是require不带_relative解析相对路径时是以当前工作目录为基准的容易因运行位置不同而报LoadError。所以对项目自有代码统一使用require_relative对标准库和 gem 才使用require例如require csv。此外被 require 的代码会进入同一命名空间重名的方法/常量会相互覆盖——这是把代码拆到多个文件后必须记住的潜在陷阱。用 RuboCop 给井字棋做一次“代码体检”完成基本功能后Linting 与 RuboCop 课程建议你对自己的 Ruby 项目运行 RuboCop 检查。井字棋是多文件、多类的第一个 OOP 项目正是体会 Metrics指标类 Cop 价值的最佳时机Metrics/AbcSize衡量方法中的赋值Assignment、分支调用Branch、条件Conditional规模。如果你写出了“又长又绕”的方法比如把落子、判胜、切回合全写在一个方法里RuboCop 会提示你拆分——这正是重构的信号Metrics/CyclomaticComplexity统计方法可走的路径数。if/elsif、逻辑运算符、循环都会累加提醒你控制分支复杂度Metrics/PerceivedComplexity进一步对控制流加权衡量人类阅读的困难程度。在项目根目录运行bundle exec rubocop如果提示Style/StringLiterals之类的风格问题可以用bundle exec rubocop -a做安全自动修正对于确实需要保留的复杂方法才使用行内注释# rubocop:disable Metrics/...临时豁免。RuboCop 与 Ruby LSP 集成后还会在 VSCode 的Problems面板实时显示问题并支持针对单行代码的 Quickfix。提交与后续从井字棋到测试驱动的 Connect Four完成实现并提交后井字棋项目在课程线中还有后续延伸为井字棋编写测试在 Ruby 测试与 RSpec 章节中你会回到这个项目用 RSpec 为它补写测试——例如“棋盘第一行是 X X X 时#game_over或等价方法应判定玩家获胜”并使用 mock/double 隔离方法、验证返回值。因此现在设计类和方法时就要让每个方法有清晰的单一职责和可预测的返回值这会让你写测试时省力得多。这与 JS 课程线中的 浏览器版井字棋项目要求先实现控制台版本、再接入 DOM在设计哲学上完全一致先让核心逻辑在纯命令行/纯数据结构层面跑通。下一个项目是 Mastermind同章节的 Mastermind 项目要求构建 12 回合内猜中密令的游戏并支持人机互换角色、为电脑实现猜谜策略——它建立在井字棋练就的类划分与状态管理能力之上。进阶是 Connect Four在 Connect Four 项目中你将以TDD测试驱动开发方式重构命令行游戏——先写失败测试、再写最小实现、再重构。井字棋中学到的“棋盘状态归 Board、流程归 Game、玩家只持数据”的模型可以直接迁移到 7 列 6 行的棋盘上。常见坑点与自查清单构建过程中下面的自查清单可以帮助你对照原文档的验收标准棋盘是否在每回合之间正确显示而不是只在结束时显示一次两名人类玩家是否轮流落子且使用的标记不同X / O是否禁止在已被占用的格子落子valid_move?需要正确处理边界横、竖、斜三种获胜方向是否都被覆盖平局棋盘满且无人获胜是否被正确处理Game是否通过Board的方法读写棋盘而不是直接操作内部数组是否每个类一个文件且用require_relative正确引用是否能用bundle exec rubocop通过或至少理解每一条 offense一个值得注意的设计细节是输入坐标的约定参考骨架使用 0-8 的一维索引对应 3×3 棋盘而许多实现会采用 1-9 的编号或(row, col)二维坐标。无论选哪种都应把“位置表示法”限定在Board内部或统一约定避免Game和Board各自维护一套坐标解释逻辑——这也是“不共享不必要信息”原则的延伸。总结本项目的价值不在“写出一个能玩的井字棋”而在于完成一次完整的对象建模 → 分文件实现 → 低耦合组装 → 代码体检流程通过思考“什么是类、什么是实例变量、什么是方法”你把一个熟悉的游戏拆解为Board、Player、Game三个职责清晰的类通过require_relative组织多文件结构体验了真实 Ruby 项目的布局惯例通过信息隔离原则让类之间只通过方法协作。这套能力将直接复用到 Mastermind、Hangman、Connect Four并最终在 Chess 项目中迎来全面检验——正如课程所说你会“开始感觉更像一个真正的程序员而且这种感觉是名副其实的”。【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻