项目:猜数字游戏
这是第一阶段(Phase 1)的总结项目。在前四章中,你已经学习了变量、运算符、函数、集合和控制流。现在到了把所有这些技能综合起来的时候了——我们将构建一个完整的交互式程序:猜数字游戏。
游戏规则如下:程序在 1 到 100 之间选一个秘密数字,玩家输入猜测值,程序告诉玩家是"太大"还是"太小",直到猜对为止。我们将分四个阶段来构建这个游戏,每次增加一个新功能:
- 读取输入 -- 从玩家那里获取猜测值并打印出来
- 添加循环 -- 让玩家反复猜测并获得反馈
- 随机数 -- 每局游戏选一个不同的秘密数字
- 错误处理 -- 优雅地处理无效(非数字)输入
准备工作
为本章创建一个新的 Auto 项目:
$ automan new guessing_game
$ cd guessing_game在编辑器中打开 src/main.at。你将在这个文件中编写所有代码。游戏的每个版本都放在这个文件中。
读取输入
第一个版本还不能玩游戏——它只是让玩家输入一个值然后回显出来。这让我们学习两个基本的内置函数:input() 和 print()。
让我们逐行分析这段代码。
print 函数
print("Guess the number!")print 是一个内置函数,用于向终端输出文本。它接受一个 str 参数,并在末尾自动添加换行符。我们调用了两次:一次用来宣布游戏开始,一次用来提示玩家输入。
input 函数
var guess = input()input() 是一个内置函数,从玩家那里读取一行文本,返回一个 str。程序会在这里暂停,等待用户输入内容并按回车键。我们将结果存储在 guess 中,用 var 声明是因为在后面的版本中我们会重新赋值。
用 + 拼接字符串
print("You guessed: " + guess)+ 运算符用于拼接两个字符串。编译器会推断结果也是 str 类型。如果你写 "a" + 42,编译器会报告类型错误——你需要先把数字转换成字符串。下一节我们会看到如何用 parse_int 做反向转换。
运行程序
$ automan run
Guess the number!
Please input your guess.
76
You guessed: 76程序打印提示信息,等待你输入,然后把输入回显出来。虽然还算不上什么游戏,但基础已经搭好了。
注意每种目标语言处理用户输入的方式不同:
- Rust 使用自定义的
read_line()函数。转译器管理返回的String。 - Python 直接映射到内置的
input()函数。这是最接近的对应——Python 的input()和 Auto 的input()功能完全一样。 - C 使用
fgets()读入固定大小的缓冲区,然后用strcspn去掉末尾的换行符。需要手动管理内存。 - TypeScript 使用
readline模块的回调 API。回调函数接收用户输入的string。
添加循环
现在玩家只能猜一次,程序就退出了。真正的游戏需要不断询问,直到玩家猜对为止。我们还需要把字符串输入转换为数字,以便与秘密数字进行比较,并给出"太大"或"太小"的反馈。
这里有很多新内容。我们逐一分析。
for true 无限循环
for true {
// ...
}for true 创建一个无限循环——它会一直运行,直到循环内部有代码显式停止它。在 Auto 中,for 是唯一的循环关键字,它处理所有循环模式。for true 的意思是"永远循环"。转译器将其映射为 Rust 的 loop、Python 的 while True、C 的 while (1),以及 TypeScript 的递归函数。
parse_int 函数
var guess = parse_int(guess_str)input() 返回的是 str,但我们需要一个数字才能与秘密数字比较。parse_int 将字符串转换为整数。如果字符串是 "76",它返回整数 76。编译器推断 guess 的类型为 int。
注意:在这个版本中,如果玩家输入的不是数字(比如 "hello"),parse_int 会导致程序崩溃。我们会在最终版本中修复这个问题。
比较运算符和 if/else
if guess == secret {
print("You win!")
break
}
if guess < secret {
print("Too small!")
} else {
print("Too big!")
}我们用 == 判断相等,用 < 判断小于。if 语句在条件为 true 时执行其代码块。else 块在 if 条件为 false 时执行。
break 关键字立即退出循环。当玩家猜对时,我们打印"You win!",然后执行 break,跳转到 for 块之后的代码——由于后面没有代码了,程序结束。
运行程序
$ automan run
Guess the number!
10
You guessed: 10
Too small!
50
You guessed: 50
Too big!
42
You guessed: 42
You win!游戏可以正常运行了。但有一个问题:秘密数字永远是 42。如果你玩两次,你已经知道答案了。让我们修复这个问题。
随机数
为了让游戏可以重复游玩,我们需要每次生成不同的秘密数字。内置函数 random_int(min, max) 在给定范围内(两端包含)生成一个随机整数。
与代码清单 5-2 相比,唯一的变化是 main 内的第一行:
var secret = random_int(1, 100)不再硬编码 42,我们调用 random_int(1, 100) 获取 1 到 100 之间的随机整数。secret 的类型被推断为 int。我们还更新了开头的提示信息,告诉玩家数字的范围。
现在每次运行程序都会得到不同的秘密数字。试试看:
$ automan run
Guess the number between 1 and 100!
50
You guessed: 50
Too big!
25
You guessed: 25
Too small!
37
You guessed: 37
You win!好多了。但还有一个问题需要解决。
错误处理
如果玩家输入的不是数字会怎样?目前 parse_int("hello") 会导致程序崩溃。这可不是好的用户体验。我们应该检测到无效输入,打印一条消息,然后让玩家重新输入。
在 Auto 中,parse_int 返回一个可选整数:?int。可选值要么是一个真正的整数,要么是 none(表示"没有值")。我们可以检查 none 来处理错误情况。
这是游戏的最终版本。让我们看看新增了什么。
可选类型和 none
var guess = parse_int(guess_str)
if guess == none {
print("Please enter a valid number!")
continue
}parse_int 现在返回 ?int(读作"可选 int")而不是普通的 int。可选类型意味着值要么是一个整数,要么是 none。当玩家输入 "hello" 时,parse_int 返回 none——没有有效的整数可以提取。当玩家输入 "42" 时,它返回 42。
我们用 guess == none 检测错误情况。如果输入无效,我们打印一条消息,然后用 continue 跳过循环体的剩余部分,开始下一次迭代,再次要求玩家输入。
在第二阶段(Phase 2)中,你将学习 Auto 的完整错误处理机制——
!T类型和 解包运算符。现在,用== none检查是处理可选值的基本方法。
continue 关键字
continue 跳过当前循环迭代的剩余部分,回到条件检查。结合 for true,它实际上就是"从循环顶部重新开始"。这非常适合跳过无效输入,而不把它算作一次猜测。
计数尝试次数
var attempts = 0
// ...
attempts = attempts + 1我们添加了一个 attempts 计数器,从 0 开始,每次玩家输入有效猜测时加 1。当玩家获胜时,我们在胜利消息中包含尝试次数。注意我们只在 none 检查之后才递增 attempts,这样无效输入不会被计入。
运行最终版本
$ automan run
Guess the number between 1 and 100!
Please input your guess.
hello
Please enter a valid number!
Please input your guess.
50
You guessed: 50
Too big!
Please input your guess.
25
You guessed: 25
Too small!
Please input your guess.
37
You guessed: 37
You win! You got it in 3 tries!游戏处理了所有边界情况:非数字输入、过大猜测、过小猜测和正确猜测。它会计数尝试次数,给出反馈,并在玩家获胜时干净地退出。
看看每种目标语言如何处理"无效输入"检查:
- Rust 使用
Option<i64>,通过.ok()(将Result转换为Option),.is_none()进行检查,.unwrap()提取值。 - Python 使用
try/except ValueError块。如果int()对无效输入失败, 会抛出ValueError,except分支来处理。 - C 使用
strtoll(),它会设置endptr指向第一个非数字字符。如果*endptr != '\0',说明输入包含非数字字符。 - TypeScript 在
parseInt()之后检查isNaN(guess)。如果输入不是有效数字,parseInt返回NaN(Not a Number)。
总结
恭喜你——你已经在 Auto 中构建了一个完整的交互式游戏!这个项目综合运用了你在第一阶段学到的所有知识:
- 变量(
var)——secret、attempts、guess、guess_str都使用var来存储游戏中会变化的值 - 函数(
fn)—— 整个游戏都在main()中,我们还调用了内置函数print()、input()、parse_int()和random_int() - 运算符 ——
==、<、+用于比较和拼接 - 控制流 ——
if/else用于分支判断,for true用于循环,break退出循环,continue跳过迭代 - 类型 ——
int、str、?int(可选整数),编译器从上下文推断类型 - 集合 —— 虽然这个游戏没有直接使用,但你在第四章已经学习了数组、对象、映射和集合
这是第一阶段的结束。你现在已经打下了 Auto 核心语法和语义的坚实基础。在第二阶段,我们将更深入:
- 第二阶段 引入
let不可变绑定、is关键字进行模式匹配、type自定义类型、ext扩展方法,以及!T完整错误处理。 - 第三阶段 涵盖泛型、接口、并发等更高级的语言特性。
花点时间回顾一下你学到的内容。试试自己修改猜数字游戏:改变数字范围、添加最大尝试次数、或者根据玩家猜对的快慢打印不同的消息。你已经掌握了所有需要的工具。