外观
第 1 章 猜拳游戏
小螃蟹说
欢迎来到第一课!今天我们要做的游戏,你肯定玩过——石头剪刀布。 我们俩要大战几回合,看看谁更厉害。不过有个小秘密:我出什么,是电脑替我随机决定的,连我自己都不知道下一秒会出什么!
这一章做完,你就拥有了一个可以和朋友对战的猜拳游戏。更重要的是,你会认识 Rust 最常用的几个"老朋友":变量、数据类型、函数和控制流。它们就像积木里的四块基础积木,以后每一个程序都离不开它们。
1.1 预览:我们要做什么
我们要做一个三局两胜的猜拳游戏:
- 你输入
石头、剪刀或布 - 小螃蟹(Ferris)随机出一个
- 电脑判断谁赢,记录比分
- 谁先赢两局,谁就赢得比赛
先看看它跑起来是什么样子:
运行结果
text
欢迎来到猜拳游戏!
小螃蟹 Ferris 陪你玩,三局两胜!
=== 第 1 回合 ===
请出拳(输入:石头 / 剪刀 / 布):
你出了:石头
小螃蟹出了:剪刀
你赢了!
=== 第 2 回合 ===
请出拳(输入:石头 / 剪刀 / 布):
你出了:布
小螃蟹出了:石头
你赢了!
你赢了比赛!你真厉害!运行结果仅供参考
我出什么完全是随机的,所以你运行的时候,过程不会和上面一模一样。这正说明程序是真的在"想",而不是放录像。每次结果不一样,才好玩!
这一章你会学到:
| 知识 | 是什么 | 会用在哪儿 |
|---|---|---|
| 变量 | 贴了名字的盒子,用来装东西 | 存你的出拳、记比分 |
| 数据类型 | 盒子里装的东西的种类 | String 存文字,u32 存数字 |
| 函数 | 可重复使用的小帮手 | 读你的出拳、判断谁赢 |
| 控制流 | 让程序自己拿主意 | if 判断输赢,loop 一直玩 |
| 外部 crate | 借别人的玩具来用 | rand 让小螃蟹随机出拳 |
1.2 动手做
步骤一:新建项目
打开终端,让 Cargo 给我们盖一间新小屋:
bash
cargo new rps_game
cd rps_gamerps_game 是 "rock paper scissors game" 的缩写,就是"石头剪刀布游戏"的意思。Cargo 的规矩是:项目名字只能用英文、数字和下划线。
先跑一下 Cargo 送我们的开场程序:
bash
cargo run运行结果
text
Compiling rps_game v0.1.0
Finished dev profile [unoptimized + debuginfo] target(s) in 0.23s
Running target/debug/rps_game.exe
Hello, world!步骤二:请 rand 来帮忙
要让小螃蟹"随机"出拳,光靠 Rust 自带的东西不够。好在有一个现成的工具叫 rand(读作"软的"),它专门负责制造随机数。
打开项目里的 Cargo.toml(小房子的户口本),在最后面加上一行:
toml
[dependencies]
rand = "0.9"现在你的 Cargo.toml 长这样:
toml
[package]
name = "rps_game"
version = "0.1.0"
edition = "2024"
[dependencies]
rand = "0.9"[dependencies] 的意思是"我请了这些帮手"。这一章之后,我们还会请很多帮手,它们都住在这个小区里。
小贴士
写完 Cargo.toml 之后,下次运行 cargo run 时,Cargo 会自动去下载 rand。如果你看到它"吭哧吭哧"下载了好一会儿,别担心,那是它在干活。下载一次之后,以后就快了。
步骤三:读出你的出拳
打开 src/main.rs,把里面的代码全部换成下面这一小段:
rust
use std::io;
fn main() {
println!("猜拳游戏开始!");
let mut input = String::new();
io::stdin().read_line(&mut input).expect("读取输入失败");
println!("你出了:{}", input);
}运行它,随便输入几个字,再按回车:
运行结果
text
猜拳游戏开始!
石头
你出了:石头这一小段代码里藏着三个新知识,我们一个一个认:
第一行 use std::io; 是在"请帮手"。std 是 Rust 的标准库(standard library),是 Rust 自带的百宝箱,里面什么都有。但标准库太大,默认只把最常用的几样带进门(这几样叫预导入,prelude)。io(输入输出)不在预导入里,所以我们要用 use 把它请进来。
let mut input = String::new(); 这里出现了我们的第一个变量。let 的意思是"让这里有个东西",input 是它的名字,String::new() 造了一个空的文字盒子。mut 我们待会儿在知识深挖里细说,现在先记住:以后想"往盒子里放东西",就要在名字前面写上 mut。
io::stdin().read_line(&mut input) 是在"听你说话"。read_line 把你敲的一整行字装进 input 这个盒子里。后面的 .expect("读取输入失败") 是个保险:万一读失败了(比如没人输入),程序就大声报错,而不是悄悄装聋作哑。
小贴士
把 println!("你出了:{}", input); 里的 {} 想成"挖好的坑",后面的 input 就是想填进去的东西。一个坑填一个,填完了,println! 这台小喇叭就把整句话喊出来。
步骤四:让小螃蟹随机出拳
现在轮到小螃蟹了。先在文件的最上面加一行,请 rand 进门:
rust
use rand::Rng;然后在 main 函数的末尾,加上两行:
rust
fn main() {
println!("猜拳游戏开始!");
let mut input = String::new();
io::stdin().read_line(&mut input).expect("读取输入失败");
println!("你出了:{}", input);
let choices = ["石头", "剪刀", "布"];
let index = rand::rng().random_range(0..choices.len());
println!("小螃蟹出了:{}", choices[index]);
}运行几次,看看小螃蟹出什么:
运行结果
text
猜拳游戏开始!
剪刀
你出了:剪刀
小螃蟹出了:布这三行又带来三个新朋友:
let choices = ["石头", "剪刀", "布"];造了一个数组(array)。你可以把它想成一排贴着编号的盒子,从0开始数:0 号盒子装着"石头",1 号盒子装着"剪刀",2 号盒子装着"布"。rand::rng()是 rand 帮我们造的随机机器,.random_range(0..choices.len())让它从 0 到数组长度之间随便挑一个数。choices.len()是问数组"你有几个盒子?"——这里有 3 个,所以随机数只可能是 0、1、2 中的一个。choices[index]是按编号拿盒子:随机数是 0,就拿"石头";是 1,就拿"剪刀"。
步骤五:判断谁赢了
现在我们要教会电脑玩猜拳——也就是判断:你出石头,小螃蟹出剪刀,谁赢?
在 main 函数的后面(大括号的外面),写一个新的函数:
rust
fn judge(player: &str, ferris: &str) -> &'static str {
if player == ferris {
"平局!"
} else if (player == "石头" && ferris == "剪刀")
|| (player == "剪刀" && ferris == "布")
|| (player == "布" && ferris == "石头")
{
"你赢了!"
} else {
"小螃蟹赢了!"
}
}然后在 main 函数里,把"打印小螃蟹出了什么"那一行改成:
rust
println!("小螃蟹出了:{}", ferris);
let result = judge(&input, ferris);
println!("{}", result);等等,ferris 是谁?我们还得把"小螃蟹随机出拳"那部分装进一个函数里。把步骤四加的那三行删掉,换成调用一个叫 random_choice 的函数,再在 judge 函数后面写上它:
rust
fn random_choice() -> &'static str {
let choices = ["石头", "剪刀", "布"];
let index = rand::rng().random_range(0..choices.len());
choices[index]
}这时候你的整个 main.rs 应该是这个样子:
rust
use std::io;
use rand::Rng;
fn main() {
println!("猜拳游戏开始!");
let mut input = String::new();
io::stdin().read_line(&mut input).expect("读取输入失败");
println!("你出了:{}", input);
let ferris = random_choice();
println!("小螃蟹出了:{}", ferris);
let result = judge(&input, ferris);
println!("{}", result);
}
fn random_choice() -> &'static str {
let choices = ["石头", "剪刀", "布"];
let index = rand::rng().random_range(0..choices.len());
choices[index]
}
fn judge(player: &str, ferris: &str) -> &'static str {
if player == ferris {
"平局!"
} else if (player == "石头" && ferris == "剪刀")
|| (player == "剪刀" && ferris == "布")
|| (player == "布" && ferris == "石头")
{
"你赢了!"
} else {
"小螃蟹赢了!"
}
}注意!
use rand::Rng; 别忘了!random_range 是 Rng 这个帮手(特征)提供的能力,就像玩具需要装电池一样,不写这行,玩具转不起来。编译报错说找不到 random_range 时,先检查它。
运行一次,现在能分出胜负了:
运行结果
text
猜拳游戏开始!
布
你出了:布
小螃蟹出了:剪刀
小螃蟹赢了!步骤六:三局两胜
单打独斗不过瘾。我们把它改成三局两胜:循环玩下去,谁先赢两局谁获胜。
把整个 src/main.rs 替换成下面的完整版:
rust
use std::io;
use rand::Rng;
fn main() {
println!("欢迎来到猜拳游戏!");
println!("小螃蟹 Ferris 陪你玩,三局两胜!");
let mut player_wins = 0;
let mut ferris_wins = 0;
let mut round = 0;
loop {
round += 1;
println!();
println!("=== 第 {} 回合 ===", round);
let player = read_player_choice();
let ferris = random_choice();
println!("你出了:{}", player);
println!("小螃蟹出了:{}", ferris);
let result = judge(&player, ferris);
println!("{}", result);
if result == "你赢了!" {
player_wins += 1;
} else if result == "小螃蟹赢了!" {
ferris_wins += 1;
}
if player_wins == 2 || ferris_wins == 2 {
break;
}
}
println!();
if player_wins == 2 {
println!("你赢了比赛!你真厉害!");
} else {
println!("小螃蟹赢了比赛!再试一次吧!");
}
}
fn read_player_choice() -> String {
loop {
println!("请出拳(输入:石头 / 剪刀 / 布):");
let mut input = String::new();
io::stdin().read_line(&mut input).expect("读取输入失败");
let choice = input.trim().to_string();
if choice == "石头" || choice == "剪刀" || choice == "布" {
return choice;
}
println!("小螃蟹没听懂,请重新输入哦。");
}
}
fn random_choice() -> &'static str {
let choices = ["石头", "剪刀", "布"];
let index = rand::rng().random_range(0..choices.len());
choices[index]
}
fn judge(player: &str, ferris: &str) -> &'static str {
if player == ferris {
"平局!"
} else if (player == "石头" && ferris == "剪刀")
|| (player == "剪刀" && ferris == "布")
|| (player == "布" && ferris == "石头")
{
"你赢了!"
} else {
"小螃蟹赢了!"
}
}这里面有两个新东西:
loop 是"转圈圈"。它让程序一遍一遍地重复做同一件事。我们每转一圈,就是玩一个回合。想让圈圈停下来,就用 break——就像你喊了一声"停!",旋转木马就停了。
read_player_choice 函数里有个保险。如果你输错了(比如输入了"香蕉"),程序不会傻掉,而是问你"没听懂,请重新输入",直到你输入正确的三种之一为止。注意看,我们顺手用 input.trim() 把输入首尾多余的空格擦掉了——这样即使你不小心多敲了个空格,也能正确识别。
运行,大战一场吧!
运行结果
text
欢迎来到猜拳游戏!
小螃蟹 Ferris 陪你玩,三局两胜!
=== 第 1 回合 ===
请出拳(输入:石头 / 剪刀 / 布):
香蕉
小螃蟹没听懂,请重新输入哦。
请出拳(输入:石头 / 剪刀 / 布):
石头
你出了:石头
小螃蟹出了:剪刀
你赢了!
=== 第 2 回合 ===
请出拳(输入:石头 / 剪刀 / 布):
石头
你出了:石头
小螃蟹出了:石头
平局!
=== 第 3 回合 ===
请出拳(输入:石头 / 剪刀 / 布):
布
你出了:布
小螃蟹出了:剪刀
小螃蟹赢了!
=== 第 4 回合 ===
请出拳(输入:石头 / 剪刀 / 布):
石头
你出了:石头
小螃蟹出了:布
你赢了!
你赢了比赛!你真厉害!猜拳游戏,完工!去拉一个朋友来对战吧。如果他们也想学,把这本书推荐给他们。
1.3 知识深挖
这一节,我们把刚才用到的知识认真认识一遍。以后每一章都有这么一节,它是你的"知识小词典"。
1.3.1 变量:贴了名字的盒子
程序要工作,就得先把东西存起来。存东西的盒子,在 Rust 里叫变量(variable)。
rust
let input = String::new();读法:"让 input 这个变量,装着一个新的空 String。" let 是"让"的意思。
盒子有个规矩:默认是锁着的。装进去之后,你想再往里面放东西,不行!
rust
let apples = 5;
apples = 6; // 报错!盒子锁着,不能换东西这看起来有点不讲理,但这是 Rust 精心设计的好规矩:盒子锁着,别人就不会偷偷换掉里面的东西,程序就不会莫名奇妙地变错。你想让盒子可以打开,就在名字前面加 mut(mutable 的缩写,意思是"可变的"):
rust
let mut input = String::new();
input = String::from("hi"); // 这次可以了为什么有这个规矩?
很多别的语言,盒子默认都能随便开。Rust 偏偏反着来:默认锁着,要用 mut 才解锁。这样,读到代码的人一眼就能看出"哪个盒子会被换内容",不容易出错。
顺便认识一个和变量长得像的亲戚:常量(constant)。
rust
const MAX_ROUNDS: u32 = 5;常量用 const 写,永远是锁死的,而且必须当场给值。它像一个永不褪色的公告牌,适合放"整个游戏都用它、从不改变"的数。我们练习里会用到它。
1.3.2 数据类型:盒子里装的东西
盒子有名字,里面的东西也有类型(type)。Rust 的编译器是位严格的门卫:盒子说好装数字,你塞进文字,它立刻拦住你,连运行的机会都不给。
我们这一章用到了三种类型:
| 类型 | 是什么 | 例子 |
|---|---|---|
String | 会变长的文字 | "hello",可以一直往后面加字 |
&str | 写死在代码里的文字 | "石头"、"剪刀" |
u32 | 正整数(没有小数、没有负号) | 0、3、99 |
String 是"活"的:它的大小会变,可以往里面加字。你输入的每一行字,都装在一个 String 里。
&str 是"死"的:它是一段固定不变的文字,比如我们写死在代码里的 "石头"。它的全名叫字符串切片(string slice),不过现在你只要记住:带引号的文字就是它。至于 & 这个符号的意思(引用),我们第 3 章再好好认识它。
u32 是数字:u 是 unsigned(无符号)的缩写,意思是"没有负号",它只能装 0 和正数。32 是说它最多能装下 32 位(bit)的数字,大约能数到 43 亿。我们的比分用它绰绰有余。
还有一个小伙伴是数组(array):一排贴着编号的盒子。
rust
let choices = ["石头", "剪刀", "布"];编号从 0 开始数,这是程序员的老规矩:
| 编号 | 0 | 1 | 2 |
|---|---|---|---|
| 盒子 | "石头" | "剪刀" | "布" |
拿盒子里的东西,用 choices[编号]:
rust
println!("{}", choices[1]); // 打印:剪刀1.3.3 函数:小帮手
函数(function)是把一段代码包起来,给它起个名字,以后想用,喊名字就行。就像厨房里的榨汁机:水果塞进去,果汁出来,不用每次重新造一台。
rust
fn judge(player: &str, ferris: &str) -> &'static str {
// 中间是判断输赢的代码
}解剖一下:
fn是"function"的缩写,宣告"这里有一个函数"judge是函数的名字,我们给它起的(意思是"裁判")- 小括号里的
player和ferris是参数(parameter):从外面塞进来的东西。每个参数都要写明类型:&str -> &'static str是返回值的声明:这个函数会送出一个&str类型的文字(字符串字面值都算'static生命周期的文字,第 10 章我们细说,现在先照抄)
函数最后一行如果没有分号,这一行就是它的返回值——果汁出口。我们的 judge 有三条出口,分别送回"平局!"、"你赢了!"和"小螃蟹赢了!":
rust
if player == ferris {
"平局!" // 没有分号,它就是返回值
} else {
"小螃蟹赢了!"
}表达式 vs 语句
表达式(expression)有值,就像 1 + 1 算完是 2;语句(statement)只做事、不返回值,比如 let。Rust 的 if 是表达式——所以 judge 里每个分支都是"送出一个值"。给返回值加个分号,值就没了,函数会报错。这是我们以后经常会踩的坑,先在心里留个印象。
调用函数,就像对榨汁机说"开工":
rust
let result = judge(&player, ferris);注意我们传的是 &player 而不是 player。为什么?因为 judge 只需要"看一眼"你的出拳,不需要把它拿走。& 的意思是"借来看看"——这就是引用(reference),第 3 章我们会和它做朋友。
1.3.4 控制流:让程序自己拿主意
程序不只会从上到下一条路走到黑,它还会自己拿主意。拿主意的方法,叫控制流(control flow)。
if 是"如果……就……":
rust
if player_wins == 2 {
println!("你赢了比赛!你真厉害!");
} else {
println!("小螃蟹赢了比赛!再试一次吧!");
}条件成立,走第一个分支;不成立,走 else。条件还能排队,用 else if:
rust
if result == "你赢了!" {
player_wins += 1;
} else if result == "小螃蟹赢了!" {
ferris_wins += 1;
}loop 是"转圈圈",break 是"喊停":
rust
loop {
// 玩一个回合……
if player_wins == 2 || ferris_wins == 2 {
break; // 有人赢了,出圈!
}
}小贴士
|| 念"或者":两个条件满足一个就行。&& 念"并且":两个都要满足。== 是"等于"(注意是两个等号,一个等号是"让",千万别弄混),!= 是"不等于",+= 1 是"自己加一"。
1.3.5 注释:写给自己的小纸条
// 后面的字,电脑会当没看见。它们是注释(comment),写给自己和别人的小纸条:
rust
let mut round = 0; // 记录玩到第几回合好代码不只给电脑看,也给以后的人看——尤其是"几个月后的你自己"。到时候你会忘了代码在干什么,小纸条能帮你想起来。
1.3.6 外部 crate:请别人帮忙
我们请的 rand,在 Rust 的世界里叫外部 crate。crate 就是"别人打包好的代码积木",放在一个叫 crates.io 的大超市里。
请帮手分两步:
- 在
Cargo.toml里写上帮手名单:rand = "0.9"(0.9 是版本号) - 在代码里用
use把它请进门:use rand::Rng;
小贴士
use 的规矩和 std 一样:标准库里不在预导入的东西,要 use;外部的 crate,更要 use。只有预导入里的那几样,才不用 use 直接用。
1.4 动脑筋练习
做完一个游戏,不动手改一改,它就还是老师的作品。下面三个练习,从易到难。每个练习做完,都要运行 cargo run 试试。
练习一:五局三胜
把三局两胜改成五局三胜。提示:只要改两个数字,一个都不用加。
点开看答案
把"谁先赢两局"改成"谁先赢三局"。判断输赢的地方要改两处,它们在 main 函数末尾离得很近:
rust
if player_wins == 3 || ferris_wins == 3 {
break;
}
}
println!();
if player_wins == 3 {等等,还有一处你发现了吗?开头的两行广告词也要改,不然游戏玩着玩着就露馅了:
rust
println!("欢迎来到猜拳游戏!");
println!("小螃蟹 Ferris 陪你玩,五局三胜!");如果你愿意,还可以用常量让这个数只写一次:
rust
const WIN_ROUNDS: u32 = 3;然后把所有写 2 的地方换成 WIN_ROUNDS。这样以后想改成十局六胜,只动一行。
练习二:显示比分
每次出拳前,先让程序喊出当前比分,像这样:
text
当前比分:你 1 : 0 小螃蟹提示:把打印比分的代码放在回合号后面,记住数字是 u32,直接往 {} 里填。
点开看答案
在 loop 里、println!("=== 第 {} 回合 ===", round); 的后面加一行:
rust
println!("当前比分:你 {} : {} 小螃蟹", player_wins, ferris_wins);运行效果:
text
=== 第 1 回合 ===
当前比分:你 0 : 0 小螃蟹
请出拳(输入:石头 / 剪刀 / 布):练习三:统计平局
加一个 draw_wins 计数器,记录平局次数,比赛结束后打印"一共平局了 N 次"。这个练习要多动几步脑筋:想一想"平局"在哪里发生,比完赛的消息在哪里打印。
点开看答案
要做三处改动。
第一步,在 main 函数开头加计数器:
rust
let mut draw_wins = 0;第二步,在 if result == "你赢了!" 的上面,加一个平局的分支:
rust
if result == "平局!" {
draw_wins += 1;
} else if result == "你赢了!" {
player_wins += 1;
} else if result == "小螃蟹赢了!" {
ferris_wins += 1;
}第三步,在比赛结束的消息后面,补上平局信息:
rust
if player_wins == 2 {
println!("你赢了比赛!你真厉害!");
} else {
println!("小螃蟹赢了比赛!再试一次吧!");
}
println!("一共平局了 {} 次。", draw_wins);注意 else if 排队时,第一个条件变成"平局",原来的 else if 就顺势往后挪——程序的判断顺序很重要,想想为什么"平局"要排在最前面?
1.5 完整代码清单
这里是本章项目的全部文件。如果你写到一半跟不上了,可以照着这里核对,或者直接抄下来。
项目结构:
text
rps_game/
├── Cargo.toml
└── src/
└── main.rs文件:Cargo.toml
toml
[package]
name = "rps_game"
version = "0.1.0"
edition = "2024"
[dependencies]
rand = "0.9"文件:src/main.rs
rust
use std::io;
use rand::Rng;
fn main() {
println!("欢迎来到猜拳游戏!");
println!("小螃蟹 Ferris 陪你玩,三局两胜!");
let mut player_wins = 0;
let mut ferris_wins = 0;
let mut round = 0;
loop {
round += 1;
println!();
println!("=== 第 {} 回合 ===", round);
let player = read_player_choice();
let ferris = random_choice();
println!("你出了:{}", player);
println!("小螃蟹出了:{}", ferris);
let result = judge(&player, ferris);
println!("{}", result);
if result == "你赢了!" {
player_wins += 1;
} else if result == "小螃蟹赢了!" {
ferris_wins += 1;
}
if player_wins == 2 || ferris_wins == 2 {
break;
}
}
println!();
if player_wins == 2 {
println!("你赢了比赛!你真厉害!");
} else {
println!("小螃蟹赢了比赛!再试一次吧!");
}
}
fn read_player_choice() -> String {
loop {
println!("请出拳(输入:石头 / 剪刀 / 布):");
let mut input = String::new();
io::stdin().read_line(&mut input).expect("读取输入失败");
let choice = input.trim().to_string();
if choice == "石头" || choice == "剪刀" || choice == "布" {
return choice;
}
println!("小螃蟹没听懂,请重新输入哦。");
}
}
fn random_choice() -> &'static str {
let choices = ["石头", "剪刀", "布"];
let index = rand::rng().random_range(0..choices.len());
choices[index]
}
fn judge(player: &str, ferris: &str) -> &'static str {
if player == ferris {
"平局!"
} else if (player == "石头" && ferris == "剪刀")
|| (player == "剪刀" && ferris == "布")
|| (player == "布" && ferris == "石头")
{
"你赢了!"
} else {
"小螃蟹赢了!"
}
}怎么运行:
bash
cd rps_game
cargo run运行检查单(每一条都打钩,你的游戏就完美了):
输入 石头、剪刀、布 都能玩
输入乱七八糟的字,程序会请我重新输入
平局不算分
有人赢满两局,比赛结束,打印获胜消息