習題 48: 更進階的使用者輸入
你的遊戲可能一路跑得很爽,不過你處理使用者輸入的方式肯定讓你不勝其煩了。每一個房間都需要一套自己的語句,而且只有使用者完全輸入正確後才能執行。你需要一個設備,它可以允許使用者以各種方式輸入語彙。例如下面的幾種表述都應該被支援才對:
- open door
- open the door
- go THROUGH the door
- punch bear
- Punch The Bear in the FACE
也就是說,如果使用者的輸入和常用英語很接近也應該是可以的,而你的遊戲要識別出它們的意思。為了達到這個目的,我們將寫一個模組專門做這件事情。這個模組裡邊會有若干個類,它們互相配合,接受使用者輸入,並且將使用者輸入轉換成你的遊戲可以識別的命令。
英語的簡單格式是這個樣子的:
- 單詞由空格隔開。
- 句子由單詞組成。
- 語法控制句子的含義。
所以最好的開始方式是先搞定如何得到使用者輸入的詞彙,並且判斷出它們是什麼。
我們的遊戲語彙
我在遊戲裡建立了下面這些語彙:
- 表示方向: north, south, east, west, down, up, left, right, back.
- 動詞: go, stop, kill, eat.
- 修飾詞: the, in, of, from, at, it
- 名詞: door, bear, princess, cabinet.
- 數字詞: 由 0-9 構成的數字。
說到名詞,我們會碰到一個小問題,那就是不一樣的房間會用到不一樣的一組名詞,不過讓我們先挑一小組出來寫程式,以後再做改進吧。
如何斷句
我們已經有了詞彙表,為了分析句子的意思,接下來我們需要找到一個斷句的方法。我們對於句子的定義是「空格隔開的單詞」,所以只要這樣就可以了:
1 2 |
|
目前做到這樣就可以了,不過這招在相當一段時間內都不會有問題。
語彙結構
一旦我們知道瞭如何將句子轉化成詞彙列表,剩下的就是逐一檢查這些詞彙,看它們是什麼類型。為了達到這個目的,我們將用到一個非常便利的 Ruby 資料結構「struct」。「struct」其實就是一個可以把一串的 attrbutes 綁在一起的方式,使用 accessor 函式,但不需要寫一個複雜的 class。它的建立方式就像這樣:
1 2 3 4 |
|
這建立了一對 (TOKEN, WORD) 可以讓你看到 word 和在裡面做事。
這只是一個例子,不過最後做出來的樣子也差不多。你接受使用者輸入,用split 將其分隔成單詞列表,然後分析這些單詞,識別它們的類型,最後重新組成一個句子。
掃描輸入資料
現在你要寫的是詞彙掃描器。這個掃描器會將使用者的輸入字符串當做參數,然後返回由多個(TOKEN, WORD) struct 組成的列表,這個列表實現類似句子的功能。如果一個單詞不在預定的詞彙表中,那它返回時 WORD 應該還在,但TOKEN 應該設置成一個專門的錯誤標記。這個錯誤標記將告訴使用者哪裡出錯了。
有趣的地方來了。我不會告訴你這些該怎樣做,但我會寫一個「單元測試(unit test)」,而你要把掃描器寫出來,並保證單元測試能夠正常通過。
Exceptions And Numbers
有一件小事情我會先幫幫你,那就是數字轉換。為了做到這一點,我們會作一點弊,使用「異常(exceptions)」來做。「異常」指的是你運行某個函數時得到的錯誤。你的函數在碰到錯誤時,就會「提出(raise)」一個「異常」,然後你就要去處理(handle)這個異常。假如你在 IRB 裡寫了這些東西:
ruby-1.9.2-p180 :001 > Integer("hell")
ArgumentError: invalid value for Integer(): "hell"
from (irb):1:in `Integer'
from (irb):1
from /home/rob/.rvm/rubies/ruby-1.9.2-p180/bin/irb:16:in `<main>'
這個 ArgumentError
就是 Integer()
函式拋出的一個異常。因為你給Integer()
的參數不是一個數字。Integer()
函數其實也可以傳回一個值來告訴你它碰到了錯誤,不過由於它只能傳回整數值,所以很難做到這一點。它不能返回-1,因為這也是一個數字。 Integer()
沒有糾結在它「究竟應該返回什麼」上面,而是提出了一個叫做TypeError
的異常,然後你只要處理這個異常就可以了。
處理異常的方法是使用 begin
和 rescue
這兩個關鍵字:
1 2 3 4 5 6 7 |
|
你把要試著運行的程式碼放到「begin」的區段裡,再將出錯後要運行的程式碼放到「except」區段裡。在這裡,我們要試著呼叫 Integer()
去處理某個可能是數字的東西,如果中間出了錯,我們就「rescue」這個錯誤,然後返回 「nil」。
在你寫的掃描器裡面,你應該使用這個函數來測試某個東西是不是數字。做完這個檢查,你就可以聲明這個單詞是一個錯誤單詞了。
What You Should Test
這裡是你應該使用的測試檔案 test/test_lexicon.rb
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
|
記住你要使用你的專案骨架來建立新專案項目,將這個 Test Case 寫下來(不許複製貼上!),然後編寫你的掃描器,直至所有的測試都能通過。注意細節並確認結果一切工作良好。
設計提示
集中一次實現一個測試,盡量保持簡單,只要把你的 lexicon.rb
詞彙表中所有的單詞放那裡就可以了。不要修改輸入的單詞表,不過你需要建立自己的新列表,裡邊包含你的語彙元組。另外,記得使用 include?
函式來檢查這些語彙陣列,以確認某個單詞是否在你的語彙表中。
加分習題
- 改進單元測試,讓它覆蓋到更多的語彙。
- 向語彙列表添加更多的語彙,並且更新單元測試程式碼。
- 讓你的掃描器能夠識別任意大小寫的詞彙。更新你的單元測試以確認其功能。
- 找出另外一種轉換為數字的方法。
- 我的解決方案用了37 行程式碼,你的是更長還是更短呢?