跳轉到內容

Smalltalk 的趣味學習 / 數字猜謎遊戲

來自 Wikibooks,開放世界中的開放書籍

在本教程中,我們將建立一個“數字猜謎遊戲”。

遊戲規則如下:某人心中想一個隨機數,你需要儘可能少地嘗試次數猜出這個數字。

每次你猜測時,你會得到以下三種答案之一:

  1. “我的數字更大”
  2. “我的數字更小”
  3. “你猜對了!”

當你最終猜出數字時,遊戲結束。

我們將從一個沒有 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) ]

現在,讓我們嘗試找出我們的方法在做什麼。我們可以得出以下結論

  1. 它確保遊戲在必要時正確地重新初始化
  2. 它執行遊戲互動的步驟
  3. 它在迴圈中重複這些步驟,直到遊戲獲勝

我們將對重新初始化和遊戲互動進行重構,將其分成這些方法

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 同時出現在 doOneIterationplay 中感覺不太對勁。問題是我們混合了抽象級別:我們為使用者互動、遊戲迭代和重新初始化建立了很好的意圖揭示方法,但我們仍然直接處理 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.

另一種方法是使用 Stringformat: 方法

'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 會遵守所有換行符、製表符等。在我們的例子中,我們有一個顯式的換行符。

現在再次玩,看看你平均需要多少次嘗試才能擊敗電腦。

現在顯示嘗試次數

玩得開心!

華夏公益教科書