如何用围棋为猜谜游戏编程
让我们通过一起完成一个实践项目来跳入Go。本教程将向你介绍一些常见的Go概念,如变量、错误处理、循环、包等,告诉你如何使用它们来解决一个经典的初级编程问题。
在本教程中,我们将用Go建立一个 "猜数字 "的游戏。在这个游戏中,程序将生成一个1到100之间的随机整数,然后提示玩家输入一个猜测。
猜中后,如果猜中的数字比随机数高或低,程序会通知棋手,并提示棋手再次猜中。否则,如果猜对了,屏幕上将打印出一条祝贺信息,程序将退出。
这里的目的是让你获得编写围棋程序的初步经验,并让你对编写围棋程序有一个基本的了解。
先决条件
你需要在你的电脑上安装Go。您可以访问Go网站,查看您的操作系统的安装说明。
项目设置
在你的文件系统的任何地方创建一个新的目录,并通过下面的命令进入该目录。
$ mkdir guessing-game
$ cd guessing-game
接下来,用一个go.mod 文件来初始化你的项目。用你的GitHub用户名替换<username> 。
$ go mod init github.com/<username>/guessing-game
最后,在项目根目录下创建一个main.go 文件,用你喜欢的文本编辑器打开它。你将在这个文件中编写本项目的所有代码。
$ vim main.go
游戏如何运作
当程序运行时,会生成一个1到100之间的随机整数。随后提示玩家猜测生成的是什么数字,如果猜测的数字高于或低于秘密数字,则向玩家提供反馈。如果猜到的数字等于秘密数字,就会打印出一条祝贺信息,然后程序退出。
让我们在下一节开始生成一个1到100之间的随机数。
生成一个随机数
为了生成一个随机数,我们将使用标准库中的math/rand 包。修改你的main.go 文件,如下所示。
main.go
package main
import (
"fmt"
"math/rand"
)
func main() {
min, max := 1, 100
secretNumber := rand.Intn(max-min) + min
fmt.Println("The secret number is", secretNumber)
}
让我们一行一行地看一下代码。第一步是声明这个文件所属的包。在这个例子中,它是main 包。main 包在 Go 中很特别,因为它是每个可执行程序的入口。
import (
"fmt"
"math/rand"
)
为了向终端打印文本,我们需要将fmt 包纳入范围。它提供了几个方法,我们可以利用这些方法将操作的结果打印到标准输出。在我们使用rand 包之前,我们还需要导入该包。当导入一个以上的包时,你可以把包的名字放在括号里。这可以避免你在每一行都重复import 关键字。
请注意,rand 包被导入为math/rand 。这是因为它作为一个子目录嵌套在math 包里面。Go中的惯例是,包名与导入路径中的最后一个元素相同。
func main(){}
正如在上一篇文章中所讨论的,main 函数是程序的入口点。当你运行程序时,它会被自动调用。
min, max := 1, 100
上面这个语句在Go中代表一个简短的变量声明,它只能在函数内部使用。在这里,我们要创建两个新的变量(min 和max),并分别给它们赋值1和100。min 和max 变量代表将生成随机数的范围。
secretNumber := rand.Intn(max-min) + min
这里是我们在min 和max 的约束范围内生成一个秘密数字。rand 包导出了一个Intn 方法,该方法返回一个介于0和其参数之间的伪随机正数,该参数应该是一个正整数。在这种情况下,参数是99 (max -min),所以可以生成的数字范围将在0和98之间。min 被添加到rand.Intn() 的输出,以便将范围改为1...99。
请注意,max 的数字在范围内是排他性的,而min 是包容性的。如果你想要1-100之间的数字,你必须将max 变量改为101 。
fmt.Println("The secret number is", secretNumber)
fmt 包暴露了一个Println 方法,该方法将其参数打印到控制台,并在末尾添加一个换行。如果有一个以上的参数,则用空格隔开。
你可以通过使用godoc 命令来查看标准库中软件包的文档。在你的终端运行这个命令,并进入http://localhost:6060,然后点击包的链接,向下滚动,直到找到你想调查的包。
这个工具的一个好处是,如果你安装了一个第三方的软件包,你也可以在这里浏览它的文档,离线且随时可用
保存你的main.go 文件,并在终端使用go run main.go 运行该程序。你应该得到一个类似于下图的输出。
$ go run main.go
The secret number is 24
$ go run main.go
The secret number is 24
$ go run main.go
The secret number is 24
$ go run main.go
The secret number is 24
看起来每次都是同一个数字被打印到屏幕上。这绝对不是我们想要的结果。但为什么会出现这种情况呢?有一个简单的解释。
rand 包生成的数字是基于一个特定的初始值,称为其种子。默认的种子是1,所以除非你改变它,否则你将总是得到相同的输出。种子需要是唯一的,并且一直在变化,这样当程序运行多次时,你会得到更好的随机值。这个初始种子的一个流行选择是以纳秒为单位的当前时间,它在每次执行时很可能是一个不同的值。
下面是如何改变rand 生成器的初始种子。
// note that `rand.Seed()` expects an `int64` type so make sure
// that whatever you pass into it produces an `int64` value.
// Also, it needs to be called before any other methods from the
// `rand` package
rand.Seed(time.Now().UnixNano())
以下是你的main.go 文件在添加了上面的代码片段后应该有的样子。
main.go
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
min, max := 1, 100
rand.Seed(time.Now().UnixNano())
secretNumber := rand.Intn(max-min) + min
fmt.Println("The secret number is", secretNumber)
}
注意在进口中引入了time 包。这是一个提供测量和显示Go中时间的功能的包。它输出了一个返回当前时间的Now() 方法和一个返回自1970年1月1日UTC以来经过的纳秒数的UnixNano() 方法,作为一个int64 值。
虽然这里使用了方法链(time.Now().UnixNano() ),但由于Go语言处理错误的方式,在Go中你不会经常看到或使用这种模式。
尝试再次运行该程序。你应该得到不同的随机数,它们应该都是1到100之间的数字。
$ go run main.go
The secret number is 91
$ go run main.go
The secret number is 77
$ go run main.go
The secret number is 84
$ go run main.go
The secret number is 6
从终端读取用户输入
下一步是让用户输入一个猜测,然后检查它是否符合预期的格式(也就是说,它必须是一个整数)。
下面是帮助我们实现这一目标的代码。
main.go
package main
import (
"bufio"
"fmt"
"math/rand"
"os"
"strconv"
"strings"
"time"
)
func main() {
min, max := 1, 100
rand.Seed(time.Now().UnixNano())
secretNumber := rand.Intn(max-min) + min
fmt.Println(secretNumber)
fmt.Println("Guess a number between 1 and 100")
fmt.Println("Please input your guess")
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
fmt.Println("An error occured while reading input. Please try again", err)
return
}
input = strings.TrimSuffix(input, "\n")
guess, err := strconv.Atoi(input)
if err != nil {
fmt.Println("Invalid input. Please enter an integer value")
return
}
fmt.Println("Your guess is", guess)
}
让我们来看看每一个新增加的内容,以便了解它们都有什么作用。
fmt.Println("Guess a number between 1 and 100")
fmt.Println("Please input your guess")
上面的几行介绍了游戏,并提示玩家输入猜测。
下一步是接受玩家的输入。bufio 和os 包是我们要用来捕捉用户输入的东西。注意它们都是在文件的顶部被导入的。
reader := bufio.NewReader(os.Stdin)
上面一行代码声明了一个名为reader 的新变量,并将其初始化为bufio.NewReader(os.Stdin) 的返回值。
bufio 的NewReader() 方法接收一个实现了io.Reader接口的值。os.Stdin 代表一个打开的File,它指向标准输入文件描述符,并且也实现了io.Reader 接口,这就是为什么我们能够将它作为参数传递给NewReader() 。
input, err := reader.ReadString('\n')
NewReader() 方法返回一个bufio.Reader结构,该结构有一个名为ReadString() 的方法,该方法接收一个分隔符(本例中是换行符\n )。这个方法读取用户的输入,直到字符串中第一次出现定界符为止,并返回两个值:一个包含到定界符为止的数据的字符串,以及一个错误(如果有的话)。前者存储在input 变量中,而后者的值(错误)存储在err 变量中。
在Go中,函数可以返回多个值(通常是一个结果和一个错误),在根据函数的返回值分配变量时,你必须考虑到每个值。
if err != nil {
fmt.Println("An error occured while reading input. Please try again", err)
return
}
上面的片段处理了使用ReadString 方法时可能出现的错误。这是Go中处理错误的习惯性方法。在使用ReadString的结果之前,会检查错误值是否不是nil 。如果是的话,就会有一条信息打印到屏幕上,然后main 函数返回,导致程序退出。
input = strings.TrimSuffix(input, "\n")
假设err 为零,程序执行将转到上面所示的下一行。记住,ReadString() 在结果值中包括分隔符。这意味着input ,除了玩家输入的内容外,还将包含换行符。
例如,如果玩家输入30并按下回车键,input 的值将是30/n。但我们不希望输入的这部分/n ,所以我们需要把它去掉。这可以通过strings 包中的TrimSuffix() 方法来实现。我们所需要做的就是传递字符串和后缀。然后它将返回所提供的不带后缀的字符串,所以30\n 将变成30 。
Go中的变量是可变的,所以你可以如上所示对其进行重新赋值。在重新赋值过程中,唯一不能改变的是变量的类型。
guess, err := strconv.Atoi(input)
下一步我们需要将input 字符串转换为整数,这样我们就可以将其与上一节中创建的secretNumber 变量进行数字比较。
要将Go中的字符串转换为整数,我们采用了strconv 包中的Atoi() 方法。这个方法试图将提供的字符串转换为整数,并返回一个整数和一个错误,分别分配给guess 和err 。
if err != nil {
fmt.Println("Invalid input. Please enter an integer value")
return
}
使用Atoi() ,很容易导致错误,必须加以适当处理。例如,如果玩家的输入包含一个字母,就没有办法将其转换为一个整数。因此我们需要停止程序,提醒玩家提供整数输入。
fmt.Println("Your guess is", guess)
但如果没有错误,玩家的猜测将被打印在屏幕上。
保存文件并使用go run main.go 运行该程序。你应该得到一个与下面类似的输出。
$ go run main.go
45
Guess a number between 1 and 100
Please input your guess
20
Your guess is 20
比较玩家的猜测和秘密数字
现在我们有了玩家的猜测和秘密数字,是时候比较它们了。如果玩家的猜测比秘密数字高或低,我们将向玩家提供反馈。如果猜测的数字等于秘密数字,我们将打印出一条祝贺信息并退出程序。
将以下代码添加到你的main 函数的结尾,在打印guess 变量的那一行下面。
main.go
//...
func main() {
// ...
if guess > secretNumber {
fmt.Println("Your guess is bigger than the secret number. Try again")
} else if guess < secretNumber {
fmt.Println("Your guess is smaller than the secret number. Try again")
} else {
fmt.Println("Correct, you Legend!")
}
}
运行该程序几次,验证代码是否按预期工作。
$ go run main.go
The secret number is 72
Guess a number between 1 and 100
Please input your guess
65
Your guess is 65
Your guess is smaller than the secret number. Try again
$ go run main.go
The secret number is 85
Guess a number between 1 and 100
Please input your guess
93
Your guess is 93
Your guess is bigger than the secret number. Try again
$ go run main.go
The secret number is 78
Guess a number between 1 and 100
Please input your guess
78
Your guess is 78
Correct, you Legend!
我们的程序大部分都能工作,但玩家只能输入一个猜测,而且无论猜测是否正确,程序都会退出。让我们改变这一行为,使玩家能够继续猜测,直到猜出正确的数字。我们还将告知玩家在赢得游戏之前的尝试次数。
改变你的main ,如下所示。
main.go
func main() {
min, max := 1, 100
rand.Seed(time.Now().UnixNano())
secretNumber := rand.Intn(max-min) + min
fmt.Println("The secret number is", secretNumber)
fmt.Println("Guess a number between 1 and 100")
fmt.Println("Please input your guess")
attempts := 0
for {
attempts++
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
fmt.Println("An error occured while reading input. Please try again", err)
continue
}
input = strings.TrimSuffix(input, "\n")
guess, err := strconv.Atoi(input)
if err != nil {
fmt.Println("Invalid input. Please enter an integer value")
continue
}
fmt.Println("Your guess is", guess)
if guess > secretNumber {
fmt.Println("Your guess is bigger than the secret number. Try again")
} else if guess < secretNumber {
fmt.Println("Your guess is smaller than the secret number. Try again")
} else {
fmt.Println("Correct, you Legend! You guessed right after", attempts, "attempts")
break
}
}
}
我们在上面创建了一个for 循环,并将所有的代码移到了提示用户输入猜测的那一行下面。
在围棋中,使用没有循环条件的for循环会产生一个无限的循环,而脱离这种循环的唯一方法是使用break 关键字,而我们在玩家猜对后就会这样做。这也会导致程序退出。
注意在循环之前的attempts 变量声明。这就是我们记录玩家在猜对之前的猜测次数的方法。当游戏获胜时,尝试的次数会被打印在祝贺的信息中。
还要注意的是,来自Atoi() 和ReadString() 方法的错误处理被稍作修改,用continue 关键字代替return ,以确保循环在收到无效输入时跳到下一次迭代,而不是退出。
如果你现在运行这个程序,你就能玩这个游戏,并在程序不退出的情况下输入几个猜测。
$ go run main.go
The secret number is 20
Guess a number between 1 and 100
Please input your guess
26
Your guess is 26
Your guess is bigger than the secret number. Try again
Please input your guess
22
Your guess is 22
Your guess is bigger than the secret number. Try again
Please input your guess
20
Your guess is 20
Correct, you Legend! You guessed right after 3 attempts
让我们通过删除在屏幕上打印出secretNumber 的那一行来结束这个游戏。最后的代码如下所示。
main.go
package main
import (
"bufio"
"fmt"
"math/rand"
"os"
"strconv"
"strings"
"time"
)
func main() {
min, max := 1, 100
rand.Seed(time.Now().UnixNano())
secretNumber := rand.Intn(max-min) + min
fmt.Println("Guess a number between 1 and 100")
fmt.Println("Please input your guess")
attempts := 0
for {
attempts++
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
fmt.Println("An error occured while reading input. Please try again", err)
continue
}
input = strings.TrimSuffix(input, "\n")
guess, err := strconv.Atoi(input)
if err != nil {
fmt.Println("Invalid input. Please enter an integer value")
continue
}
fmt.Println("Your guess is", guess)
if guess > secretNumber {
fmt.Println("Your guess is bigger than the secret number. Try again")
} else if guess < secretNumber {
fmt.Println("Your guess is smaller than the secret number. Try again")
} else {
fmt.Println("Correct, you Legend! You guessed right after", attempts, "attempts")
break
}
}
}
结语
恭喜你!你已经成功地创建了一个猜谜游戏。你已经成功地用Go创建了一个猜谜游戏,并在此过程中学习了许多新概念,如变量、循环、函数、控制流、错误处理等。