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 會遵守所有換行符、製表符等。在我們的例子中,我們有一個顯式的換行符。
現在再次玩,看看你平均需要多少次嘗試才能擊敗電腦。

玩得開心!