内容简介:Rust 初学者指南运用您的 Rust 技能构建一个简单的井字棋游戏我在本系列第一部分中已经提到过,我非常喜欢 Rust。这种静态编译语言是内存安全且与操作系统无关的,所以它可以在任何计算机上运行。Rust 带来了系统语言的速度和低阶优势,而没有 C# 和 Java 等语言中麻烦的垃圾收集。
Rust 初学者指南
开始使用 Rust 语言进行编码
运用您的 Rust 技能构建一个简单的井字棋游戏
系列内容:
此内容是该系列 # 部分中的第 # 部分: Rust 初学者指南
https://www.ibm.com/developerworks/cn/library/?series_title_by=**auto**
敬请期待该系列的后续内容。
此内容是该系列的一部分: Rust 初学者指南
敬请期待该系列的后续内容。
我在本系列第一部分中已经提到过,我非常喜欢 Rust。这种静态编译语言是内存安全且与操作系统无关的,所以它可以在任何计算机上运行。Rust 带来了系统语言的速度和低阶优势,而没有 C# 和 Java 等语言中麻烦的垃圾收集。
要学习一种语言,最好的方法就是开始实际使用它。本文通过展示如何使用 Rust 构建一个简单的井字棋游戏,帮助您运用该语言。跟随我构建您自己的有趣游戏吧。
前提条件
请先阅读“Rust 初学者指南”系列的第一部分。我在其中展示了如何安装并运行 Rust,介绍了它的核心功能,还介绍了您需要掌握的一些入门概念。在本文中,我不会介绍该语言的每个方面,所以您需要掌握该语言的基础知识。
启动项目
首先,您需要设置您的项目。可以使用 Cargo 从终端创建新的二进制可执行程序:
$ cd ~/Documents $ cargo new tic_tac_toe –bin
在 tree 程序中,新的 tic_tac_toe 目录如下所示:
$ cd tic_tac_toe $ tree . . ??? Cargo.toml ??? src ??? main.rs
main.rs 文件应包含以下几行内容:
fn main() { println!("Hello, world!"); }
运行该程序就像创建它一样简单,如所示。
清单 1. 运行“Hello, World!”
$ cargo build Compiling … Finished … $ cargo run Finished … Running … Hello, world!
现在,您还需要为游戏模块创建一个文件。执行以下命令行来创建此文件:
$ touch ./src/game.rs
创建了项目和目录后,就可以深入研究游戏的轮廓了。
规划游戏的类型和结构
经典的井字棋游戏由两个主要组件组成:一个棋盘和每位玩家的轮次。棋盘实质上是一个空的 3x3 数组,轮次表明哪位玩家必须落子。要将此功能转变为代码,必须编辑上一节中创建的 game.rs 文件(参见清单 2)。
清单 2. 针对棋盘和玩家轮次进行了修改的 game.rs
type Board = Vec<Vec<String>>; enum Turn { Player, Bot, } pub struct Game { board: Board, current_turn: Turn, }
您可能已经注意到这里的奇怪语法,但不必担心:我会逐步介绍它。
棋盘
要将游戏棋盘转变为代码,可以使用 type
关键字设置名称 Board
的别名,以便与类型 Vec<Vec<String>>
同义。现在, Board
是一个二维字符串矢量的简单类型。我会在这里使用一个字符,因为该数组中的值只有 x
、 o
或表示一个开放位置的数字。
轮次
轮次表示哪位玩家必须选择一个位置,所以 enum
结构非常适合。在每轮中,只需匹配 Turn
变量来执行合适的方法调用。
游戏
最后,您必须创建一个包含棋盘和当前轮次的 Game
对象。但是等等! Game
结构的方法在哪里?别担心:接下来就会介绍。
实现游戏
井字棋游戏由哪些方法组成?有许多轮次。在每轮中,都会显示棋盘,一个玩家落一颗棋子,再次显示棋盘,然后检查获胜条件。如果游戏分出胜负,该游戏会宣布哪位玩家获胜,并邀请他或她再玩一局。如果没有人在游戏中获胜,该游戏会切换当前玩家并进入游戏的下一轮次。显然,根据具体的玩家,每一次落子都涉及一些细微问题,您可以从这里开始钻研。
首先,创建一个嵌套在 impl
代码块中的构造,如清单 3 所示。
清单 3. game 构造
impl Game { pub fn new() -> Game { let first_row = vec![ String::from("1"), String::from("2"), String::from("3")]; let second_row = vec![ String::from("4"), String::from("5"), String::from("6")]; let third_row = vec![ String::from("7"), String::from("8"), String::from("9")]; Game { board: vec![first_row, second_row, third_row], current_turn: Turn::Player, } } }
静态方法 new
创建并返回一个 Game
构造。这是 Rust 中的对象构造函数的标准名称。
您必须将 board
成员变量与一个 String
对象的 2d
矢量绑定在一起。请注意,我在每个位置中都填入了一个数字,表明每次落子的可用位置,而不是将每个位置都留空。接下来,将 current_turn
成员变量绑定到 Turn::Player
的值。这一行表示每局游戏都让玩家先走。
您如何玩该游戏?
第一个方法用作程序的映射。您将此方法(连同本节的剩余方法)添加到 impl Grid
代码块 内 。清单 4 给出了该方法。
清单 4. 游戏程序的映射
pub fn play_game(&mut self) { let mut finished = false; while !finished { self.play_turn(); if self.game_is_won() { self.print_board(); match self.current_turn { Turn::Player => println!("You won!"), Turn::Bot => println!("You lost!"), }; finished = Self::player_is_finished(); self.reset(); } self.current_turn = self.get_next_turn(); } }
很容易看出该游戏的流程。使用一个无限循环,从一轮前进到下一轮,并轮换 current_turn
。出于这个原因,您将在 self
上使用了一个可变借用,因为游戏的内部状态会在每轮中发生更改。
这个 enum
已获得了回报,因为如果游戏分出胜负,就会嵌入有关谁赢得游戏的信息。然后让玩家知道他或她是赢了还是输了。此外,您将棋盘重置到原始状态,这在用户想再玩一局时很有帮助。
请注意,这将是 new
以外的唯一一个 pub
方法。这意味着在使用 Game
对象时, play_game
和 new
是另一个库能够访问的唯一方法。其他所有方法都是私有的,无论它们是否是静态方法。
互换位置
play_game
方法中使用的第一个帮助器方法是 play_turn
。清单 5 给出了这个小巧的函数。
清单 5. play_turn 函数
fn play_turn(&mut self) { self.print_board(); let (token, valid_move) = match self.current_turn { Turn::Player => ( String::from("X"), self.get_player_move()), Turn::Bot => ( String::from("O"), self.get_bot_move()), }; let (row, col) = Self::to_board_location(valid_move); self.board[row][col] = token; }
这个函数比较复杂。首先,您要输出一个棋盘,让用户知道哪些位置可以落子(甚至在轮到机器人落子时也很有用)。接下来,根据 current_turn
变量,您可以使用元组解构和 match
来分配变量 token
和 valid_move
。
对于玩家或机器人, token
分别为 String X
或 O
。 valid_move
为 1 到 9 的整数,表示棋盘上未占用的位置。然后,使用 to_board_location
静态方法,将此变量转换为棋盘的相应行和列。(带有大写“S”的 Self
返回一个 self
类型 - 在本例中为 Game
。)
让我们看看棋盘
设置 play_turn
后,您需要一个输出方法。清单 6 给出了该方法。
清单 6. 输出游戏棋盘
fn print_board(&self) { let separator = "+---+---+---+"; println!("\n{}", separator); for row in &self.board { println!("| {} |\n{}", row.join(" | "), separator); } print!("\n"); }
在此方法中,您使用一个 for
循环来输出棋盘上各行的 ASCII 表示。临时变量 row
是对棋盘上每个矢量的引用。通过使用 join
方法,您可以将 row
转换为 String
,并输出新值和一个附加的分隔符 String
。
输出功能生效后,最后可以继续为玩家和机器人实现有效落子。
玩家,轮到您了
目前,此程序是一系列硬编码的返回值,没有来自玩家的输入。清单 7 将改变这一状况。
清单 7. 设置轮换
fn get_player_move(&self) -> u32 { loop { let mut player_input = String::new(); println!( "\nPlease enter your move (an integer between \ 1 and 9): "); match io::stdin().read_line(&mut player_input) { Err(_) => println!( "Error reading input, try again!"), Ok(_) => match self.validate(&player_input) { Err(err) => println!("{}", err), Ok(num) => return num, }, } } }
此方法的核心可归纳为:除非玩家为游戏提供了一步有效的落子,否则它会无限循环。
用户提示后的第一个 match 表达式尝试将用户的输入读到一个 String
( player_input
) 中,并检查这样做是否会出错。 io
模块提供了这项功能;您必须在 game.rs 文件 顶部 导入此模块。它的 stdin().read_line
方法 ( stdin()
) 向当前标准输入返回一个句柄对象。这是我导入 io
模块的语句:
use std::io;
同样值得注意的是,尽管 read_line
方法修改了给定的 String
,但它也返回了一个名为 Result
的 enum
。我在介绍性文章中没有谈到 Result
,所以接下来讲一下它。
Result enum
Result
被认为是一种 代数类型 。它是一个 enum
,包含两个变量: Ok
和 Err
。每个变量都可以包含数据,比如 String
或 i32
。
对于 read_line
,返回的 Result
是一个来自 io
模块的特殊版本,这意味着 Err
是一个特殊的 io::Error
变量。相较而言, Ok
与原始的 Result
变量相同,而且在这里包含一个表示读取的字节数的整数。 Result
是一个有用的 enum
,有助于确保您在编译时而不是运行时解决所有可能的错误。
另一个普遍存在于 Rust 中的类似 enum
是 Option
。它的变量为 None
(不含数据)和 Some
(含数据),而不是 Ok
和 Err
。 Option
很有用,其用途类似于 C++
中的 nullptr
或 Python 中的 None
。
Option
和 Result
之间有何区别,应在何时使用它们?以下是我认可的答案。首先,如果您期望一个函数什么都不返回,那么可以使用 Option
。如果您期望函数总是成功但可能失败,这意味着必须捕获错误,那么可以使用 Result
。明白了吗?很好。返回到 get_player_move
方法。
返回到游戏中
前面讲到从玩家读取输入。如果读取用户输入时发生错误,程序会告知用户并要求他或她重新输入。如果未发生错误,程序会到达第二个 match
表达式。请注意下划线 ( _
) 的使用:它们告诉 Rust,不同于在第二个 match 表达式中的操作,您没有将数据绑定到 Result
的 Ok
或 Err
变量内。
这个 match
表达式会检查 player_input
变量是否有效。如果该变量是无效的,那么代码会返回一个错误(游戏会提醒玩家注意该错误),并请求玩家提供有效的输入。如果 player_input
有效的,则返回该输入(使用 validate
方法转换为一个整数)。
验证您的代码
编写了游戏的核心代码后,编写一个 validate
函数会很不错。清单 8 给出了相关代码。
清单 8. Validate 函数
fn validate(&self, input: &str) -> Result<u32, String> { match input.trim().parse::<u32>() { Err(_) => Err( String::from( "Please input a valid unsigned integer!")), Ok(number) => { if self.is_valid_move(number) { Ok(number) } else { Err( String::from( "Please input a number, between \ 1 and 9, not already chosen!")) } } } }
通过逐行浏览此输出,可以得出该方法的大体含义如下。
首先,程序返回一个 Result enum
。我没有介绍类型模板,但实质上,您指示了 Result
的 Ok
变量必须包含一个 u32
整数, Err
变量必须包含一个 String
。为什么这里会返回一个 Result
?该方法预计会通过,仅在给定输入满足以下条件时才会抛出错误:
- 不是整数;
- 由于被占用而不是一个有效的位置;或者
- 由于该整数不在 1–9 内而不是一个有效的位置。
接下来,程序尝试使用 input
的 parse
方法将 input
转换为 u32
。 turbofish, ::<type>
是一些函数的一个特殊方面,它会告诉这些函数返回何种类型。在本例中,它告诉 parse
尝试将 input
转换为 u32
,同时设置 Result
的 Ok
变量来包含一个 u32
。如果 input
无法转换,代码会返回一个错误,表明 input
不是一个无符号整数。但是,如果成功转换,代码会将 input
传递给另一个帮助器函数: is_valid_move
。
为什么有另一个用于验证的帮助器函数?在之前的可能错误列表中,第一个是特定于用户的。机器人始终会提供一个整数。因此,您只需使用 validate
来验证玩家的响应。 is_valid_move
检查另外两个可能的错误。
清单 9 给出了验证代码的最后部分。
清单 9. 更多验证
fn is_valid_move(&self, unchecked_move: u32) -> bool { match unchecked_move { 1...9 => { let (row, col) = Self::to_board_location( unchecked_move); match self.board[row][col].as_str() { "X" | "O" => false, _ => true, } } _ => false, } }
非常简单。如果给定的 unchecked_move
不在 1 和 9(含 1 和 9)之间,那么它不是一次有效的落子。否则,代码必须检查该位置是否已落子。像之前在 play_turn
中一样,您将 unchecked_move
转换为棋盘上的相应行和列。然后可以检查该位置是否在棋盘上。如果该位置为 X
或 O
,则落子是无效的。
轮到机器人了
在编写方法来实现机器人的落子之前,请创建清单 10 中所示的 to_board_location
静态方法。
清单 10. to_board_location 方法
fn to_board_location(game_move: u32) -> (usize, usize) { let row = (game_move - 1) / 3; let col = (game_move - 1) % 3; (row as usize, col as usize) }
此方法稍微有点欺骗性,因为众所周知,在 validate
和 play_turn
中调用 to_board_location
时,参数 game_move
是介于 1 和 9(含 1 和 9)之间的整数。您将此方法设置为静态方法,因为此数学运算与 Game
对象毫无关系。井字棋的棋盘始终为 3x3。
对弈机器人
您的代码可以实现玩家的落子,但是如何实现机器人的落子呢?首先,机器人的落子应该是一个随机数,这意味着您需要导入第三方 crate rand
。其次,您不断生成随机落子,直到使用 is_valid_move
方法确定它到达一个有效位置。然后,该游戏必须向玩家告知机器人的落子,并返回该落子的信息。
您导入该 rand
crate 并安装在一个名为 Cargo.toml 的文件中,使用 rand
作为依赖项。清单 11 给出了该文件。
清单 11. Cargo.toml
[package] name = "tic_tac_toe" version = "0.1.0" authors = ["Dylan Hicks <dirtgrub.dylanhicks@gmail.com>"] [dependencies] rand = "0.4"
main.js 文件告诉 Cargo,您想使用此依赖项。我将此命令放在文件顶部:
extern crate rand;
然后,将此命令放在 game.rs 文件的顶部,放在 io
导入语句上方:
use rand;
有了 rand
crate 来生成随机数字后,您需要一个方法来实现机器人的落子。清单 12 给出了该方法。
清单 12. bot_move 方法
fn get_bot_move(&self) -> u32 { let mut bot_move: u32 = rand::random::<u32>() % 9 + 1; while !self.is_valid_move(bot_move) { bot_move = rand::random::<u32>() % 9 + 1; } println!("Bot played moved at: {}", bot_move); bot_move }
这很简单,对吧?
该方法使用了 play_turn
方法的依赖项。 现在,您需要创建一个方法来检查游戏是否分出胜负。
我们是冠军
现在,您将快速轻松地执行一些布尔代数运算 ()。
清单 13. 一些布尔代数运算
fn game_is_won(&self) -> bool { let mut all_same_row = false; let mut all_same_col = false; for index in 0..3 { all_same_row |= self.board[index][0] == self.board[index][1] && self.board[index][1] == self.board[index][2]; all_same_col |= self.board[0][index] == self.board[1][index] && self.board[1][index] == self.board[2][index]; } let all_same_diag_1 = self.board[0][0] == self.board[1][1] && self.board[1][1] == self.board[2][2]; let all_same_diag_2 = self.board[0][2] == self.board[1][1] && self.board[1][1] == self.board[2][0]; (all_same_row || all_same_col || all_same_diag_1 || all_same_diag_2) }
在 for
循环中,会同时检查各行和各列,查看是否满足 井字棋 的获胜条件(即 3 个 X 或 O 排成一排)。可以通过 |=
完成此任务,这类似于 +=
,但它使用了 or 运算符而不是加法运算符。然后,检查两个对角上是否包含相同的字符。最后,使用一些布尔代数运算来返回是否满足任何获胜条件。再编写 3 个方法,您就完成了此次编码。
您想再玩一次吗?
如果返回并查看中的 play_game
方法,就会看到代码在一直循环,直到 finished
为 true
。只有在方法 player_is_finished
为 true
时才会出现这种情况。此方法应该基于玩家的响应:yes 或 no ()。
清单 14. player_is_finished 方法
fn player_is_finished() -> bool { let mut player_input = String::new(); println!("Are you finished playing (y/n)?:"); match io::stdin().read_line(&mut player_input) { Ok(_) => { let temp = player_input.to_lowercase(); temp.trim() == "y" || temp.trim() == "yes" } Err(_) => false, } }
在最初编写此方法时,我断定最好的方法是仅处理玩家输入的“yes”情况,这意味着其他所有输入都返回 false
。这同样是一个静态方法,因为它没有使用 self
带来的任何数据。
一次硬重置可以修复所有问题
play_game
中使用的最后一个方法是 reset
,如清单 15 所示。
清单 15. reset 方法
fn reset(&mut self) { self.current_turn = Turn::Player; self.board = vec![ vec![ String::from("1"), String::from("2"), String::from("3")], vec![ String::from("4"), String::from("5"), String::from("6")], vec![ String::from("7"), String::from("8"), String::from("9")], ]; }
此方法的唯一功能就是将游戏的成员变量设置回它们的默认值。
要完成游戏,还需要最后一个方法,即 get_next_turn
,如清单 16 所示。
清单 16. get_next_turn 方法
fn get_next_turn(&self) -> Turn { match self.current_turn { Turn::Player => Turn::Bot, Turn::Bot => Turn::Player, } }
此方法仅检查 self
是谁的轮次,并返回对方的轮次。
运行并编译游戏
完成 game.rs 模块后,您现在可以编译 main.rs 并玩游戏了 ()。
清单 17. 编译游戏
extern crate rand; mod game; use game::Game; fn main() { println!("Welcome to Tic-Tac-Toe!"); let mut game = Game::new(); game.play_game(); }
就这么简单。您通过 mod
声明了游戏模块位于此项目中,通过 use
将 Game
对象引入作用域内。然后,您通过 Game::new()
创建了一个 game
对象,并告诉该对象玩此游戏。现在,通过 Cargo 运行它 ()。
清单 18. 运行游戏
$ cargo run Compiling tic_tac_toe v0.1.0 … Finished dev [unoptimized + debuginfo] … Running … Welcome to Tic-Tac-Toe! +---+---+---+ | 1 | 2 | 3 | +---+---+---+ | 4 | 5 | 6 | +---+---+---+ | 7 | 8 | 9 | +---+---+---+ Please enter your move (an integer between 1 and 9): ……
最后的思考
学完整篇教程后就会知道,Rust 是一种通用语言,它既拥有 Java、 C#
或 Python 的易用性,又拥有 C
或 C++
的速度和强大功能。此代码不仅已编译并运行快速,而且所有内存和错误问题都已在编译时得到解决,而不是留到运行时,并减少了代码中可能的人为错误。
后续行动
- 要查看我为本文创建的代码,请访问我的 GitHub 存储库 。
以上所述就是小编给大家介绍的《Rust 初学者指南: 开始使用 Rust 语言进行编码》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
与孩子一起学编程
[美] 桑德Warren Sande、Carter Sande / 苏金国、姚曜 等 / 人民邮电出版社 / 2010-11 / 65.00元
一本老少咸宜的编程入门奇书!一册在手,你完全可以带着自己的孩子,跟随Sande父子组合在轻松的氛围中熟悉那些编程概念,如内存、循环、输入和输出、数据结构和图形用户界面等。这些知识一点儿也不高深,听起来备感亲切,书中言语幽默风趣而不失真义,让学习过程充满乐趣。细心的作者还配上了孩子们都喜欢的可爱漫画和经过运行测试的程序示例,教你用最易编写和最易理解的Python语言,写出你梦想中的游戏程序。 ......一起来看看 《与孩子一起学编程》 这本书的介绍吧!