Smalltalk 的趣味学习 / 数字猜谜游戏
在本教程中,我们将创建一个“数字猜谜游戏”。
游戏规则如下:某人心中想一个随机数,你需要尽可能少地尝试次数猜出这个数字。
每次你猜测时,你会得到以下三种答案之一:
- “我的数字更大”
- “我的数字更小”
- “你猜对了!”
当你最终猜出数字时,游戏结束。
我们将从一个没有 UI 的版本开始,然后使它更具交互性。
在我们的例子中,是计算机“思考”了这个随机数。但是,我们如何在 Smalltalk 中开始生成随机数呢?
通过阅读 Random
类的帮助文档,我们发现了这两种方便的用法
(6 to: 12) atRandom.
10 atRandom.
你可以在工作区中试一下。
让我们从创建一个 NumberGuessingGame
类开始。它应该有一个实例变量 number
,用来保存玩家将尝试猜的随机数。
以下是类的模板
Object subclass: #NumberGuessingGame
instanceVariableNames: 'number'
classVariableNames: ''
poolDictionaries: ''
category: 'Smalltalk-Fun'
我们将如下实现初始化方法
initialize
number := (1 to: 100) atRandom.
现在,我们将直接通过工作区与游戏进行交互。转到工作区,创建一个游戏实例
game := NumberGuessingGame new.
但是我们还不能玩!我们想进行猜测并接收答案。它应该像这样工作
game responseForGuess: 34. "My number is bigger"
game responseForGuess: 70. "My number is smaller"
该方法的一个简单的实现可能如下所示
responseForGuess: guess
(number > guess) ifTrue: [^ 'My number is bigger'].
(number < guess) ifTrue: [^ 'My number is smaller'].
^ 'You guessed right!'
现在游戏可以开始了。
在工作区中试一下,通过输入猜测并“打印”查看答案。
虽然可以通过在工作区中打印消息发送来玩游戏,但这并不像它可能的那样吸引人。让我们让它变得更“交互式”。
对于基本的交互使用,例如显示警报或请求信息,可以使用 UIManager
类。系统中已经存在一个“默认”(单例)实例,可以随时使用。
首先,我们将让游戏向我们询问一个数字。请求输入的方法是使用 request:
方法。它将显示一个对话框,用户可以在其中输入一些文本。调用将返回该文本作为字符串。在工作区中尝试一下,并确保你“打印”了结果值
UIManager default request: 'What is your name?'. "James Bond"
对话框如下所示
其次,我们将让游戏在对话框中告诉我们响应是什么。这次方法是 inform:
。扩展上面的例子,选择并评估这些语句
| name |
name := UIManager default request: 'What is your name?'.
UIManager default inform: 'Hello, ', name.
它应该像这样
有了这些知识,我们可以实现我们相应的代码。一个用于请求数字猜测,另一个用于向玩家显示响应
askForGuess
^ (UIManager default request: 'What is your guess?') asNumber.
showResponseToGuess: guess
| response |
response := self responseForGuess: guess.
UIManager default inform: response.
还记得 request:
的返回值是 String
类型吗?这就是为什么我们使用 asNumber
将其转换为数字。
看来我们可以一起使用这两个方法了。让我们在新的方法中这样做
play
| guess |
guess := self askForGuess.
self showResponseToGuess: guess.
继续在工作区中试一下
game play.
以下是猜测输入的外观
以下是响应的外观
你可能已经注意到,我们的游戏很短暂。它只询问和响应一次。然后它停止了。
我们需要反复执行此操作,直到正确猜测到数字。在 Smalltalk 中重复执行语句块的方法恰恰是使用 Block
类。查看一下这个类,尤其是在“控制”类别中。
我们将使用 doWhileFalse:
方法来不断询问,直到猜测等于随机数。我们使用 doWhileFalse:
而不是 whileFalse:
,因为我们希望我们的块至少执行一次,这样“猜测”就至少有一个初始值。
play
的新版本如下所示
play
| guess |
[ guess := self askForGuess.
self showResponseToGuess: guess ] doWhileFalse: [ guess = number ].
试一下
game play.
现在游戏循环已经就位。唯一的问题是游戏会一直“记住”初始的随机数。即使在你获胜之后。
当然,一个解决方案是在工作区中“重新初始化”游戏,如下所示
game initialize.
如果这作为我们游戏循环的一部分会更好... 但是究竟在哪里呢?让我们看看我们的选择
- 在游戏开始之前吗?看起来不太对,因为我们在游戏初始化时已经选择了随机数。
- 在游戏结束后吗?看起来也不太对,因为你可能想在获胜后询问随机数。
这似乎是一个两难选择。如果我们只是可以知道游戏是否获胜... 那么我们可以在开始时重新初始化游戏,只有在之前获胜的情况下。
我们只需要将这些信息添加到我们的游戏中。
为了存储游戏状态,我们将引入一个新的实例变量。修改类使其看起来像这样
Object subclass: #NumberGuessingGame
instanceVariableNames: 'number finished'
classVariableNames: ''
poolDictionaries: ''
category: 'Smalltalk-Fun'
变量 finished
最初将为 false
,当游戏获胜时将变为 true
。
相应地,将初始化方法更改为以下内容
initialize
number := (1 to: 100) atRandom.
finished := false.
现在我们已准备好对“游戏循环”进行必要的更改。
我们的第一个改变将是
play
| guess |
finished ifTrue: [self initialize].
[ guess := self askForGuess.
self showResponseToGuess: guess ] doWhileFalse: [ guess = number ].
这将确保如果我们重新开始游戏,它将在必要时重新初始化自身。请记住,在初始化中,我们将 finished
设置回 false
。
但是,缺少一些东西:我们在游戏获胜后从未将“finished” 设置为 true
!让我们这样做
play
| guess |
finished ifTrue: [self initialize].
[ guess := self askForGuess.
self showResponseToGuess: guess ] doWhileFalse: [ guess = number ].
finished := true.
“play” 方法现在可以满足我们的要求。但是,意图在过多的低级细节中有所丢失。这与 Smalltalk 的做事方式背道而驰,可以使用一些重构。
我们将尝试将概念分离到不同的、意图清晰的方法中。但在我们这样做之前,请允许我修改 finished
的赋值,使其局限于重复的块中。你将在稍后看到原因
play
| guess |
finished ifTrue: [self initialize].
[ guess := self askForGuess.
self showResponseToGuess: guess.
finished := (guess = number) ] doWhileFalse: [ finished ]
此更改的第一个结果是,我们可以切换到更直观的 whileFalse:
(至少对于那些熟悉其他编程语言的人来说是这样)。由于我们不再在条件中使用“guess”,我们可以像这样重新组织我们的循环
play
| guess |
finished ifTrue: [self initialize].
[ finished ] whileFalse: [
guess := self askForGuess.
self showResponseToGuess: guess.
finished := (guess = number) ]
现在,让我们尝试找出我们的方法在做什么。我们可以得出以下结论
- 它确保游戏在必要时正确地重新初始化
- 它执行游戏交互的步骤
- 它在循环中重复这些步骤,直到游戏获胜
我们将对重新初始化和游戏交互进行重构,将其分成这些方法
reinitializeIfNeeded
finished ifTrue: [ self initialize ]
doOneIteration
| guess |
guess := self askForGuess.
self showResponseToGuess: guess.
finished := (guess = number).
重构后的 play
版本如下所示
play
self reinitializeIfNeeded.
[ finished ] whileFalse: [ self doOneIteration ]
finished
同时出现在 doOneIteration
和 play
中感觉不太对劲。问题是我们混合了抽象级别:我们为用户交互、游戏迭代和重新初始化创建了很好的意图揭示方法,但我们仍然直接处理 finished
变量。这并不优雅,我们也可以改进它。
为此,我们创建以下方法
isFinished
^ finished
beFinished
finished := true.
并相应地修改调用位置
doOneIteration
| guess |
guess := self askForGuess.
self showResponseToGuess: guess.
(guess = number) ifTrue: [self beFinished ]
play
self reinitializeIfNeeded.
[ self isFinished ] whileFalse: [ self doOneIteration ]
在你继续进行并尝试游戏之前,确保你最后一次在工作区重新初始化游戏实例
game initialize.
否则,当你尝试“玩”时会得到一个错误。这是因为我们引入了一个新的变量 (finished
),它在我们的现有实例中保持未初始化 (值为 nil
)。或者,你可以创建一个全新的实例
game := NumberGuessingGame new.
现在我们已经准备好再次玩游戏,我们可以玩任意多次。
这个游戏的目标不仅是猜出一个数字,而且是“尽可能少地尝试”来猜出数字。
为了使游戏更具挑战性,我们可以在最后告诉用户尝试了多少次。让我们为此引入一个新的实例变量
Object subclass: #NumberGuessingGame
instanceVariableNames: 'number finished tries'
classVariableNames: ''
poolDictionaries: ''
category: 'Smalltalk-Fun'
不要忘记初始化它
initialize
number := (1 to: 100) atRandom.
finished := false.
tries := 0.
每次玩家猜测时,这个计数器应该增加一。我们坚持我们不希望在没有揭示意图的情况下操作实例变量,所以我们创建了这个方法
increaseTries
tries := tries + 1
并调用它
doOneIteration
| guess |
guess := self askForGuess.
self showResponseToGuess: guess.
self increaseTries.
(guess = number) ifTrue: [self beFinished ]
最后的消息也需要改变。它应该提到尝试次数。
一种方法是将两个字符串连接起来
'Number of tries: ', tries asString.
另一种方法是使用 String
的 format:
方法
'Number of tries: {1}' format: {tries}.
再一种方法是使用流
'' writeStream
nextPutAll: 'Number of tries: ';
nextPutAll: tries;
contents.
为了更好地说明,我们将最后两种方法结合起来。它看起来像这样
resposeForGuess: numberGuess
number > numberGuess
ifTrue: [ ^ 'My number is bigger' ].
number < numberGuess
ifTrue: [ ^ 'My number is smaller' ].
^ '' writeStream
nextPutAll: 'You guessed right!';
cr;
nextPutAll: ('Number of tries: {1}' format: {tries});
contents
它非常正确并且面向对象,但对于仅仅连接两个字符串和一个数字来说,它看起来相当复杂。
当然,那一部分也可以这样写
^ 'You guessed right!
Number of tries: {1}' format: {tries}.
这取决于你。
但是,如果你选择这个变体,你必须注意“尝试次数...”这句话确实在下一行的开头。这是因为在字符串文字中,Smalltalk 会遵守所有换行符、制表符等。在我们的例子中,我们有一个显式的换行符。
现在再次玩,看看你平均需要多少次尝试才能击败电脑。
玩得开心!