|
習題 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 構成的數字。
說到名詞,我們會碰到一個小問題,那就是不一樣的房間會用到不一樣的一組名詞,不過讓我們先挑一小組出來寫程式,以後再做改進吧。
如何斷句
我們已經有了詞彙表,為了分析句子的意思,接下來我們需要找到一個斷句的方法。我們對於句子的定義是「空格隔開的單詞」,所以只要這樣就可以了:
- stuff = gets.chomp()
- words = stuff.split()
复制代码
目前做到這樣就可以了,不過這招在相當一段時間內都不會有問題。
語彙結構
一旦我們知道瞭如何將句子轉化成詞彙列表,剩下的就是逐一檢查這些詞彙,看它們是什麼類型。為了達到這個目的,我們將用到一個非常便利的 Ruby 資料結構「struct」。「struct」其實就是一個可以把一串的 attrbutes 綁在一起的方式,使用 accessor 函式,但不需要寫一個複雜的 class。它的建立方式就像這樣:
- Pair = Struct.new(:token, :word)
- first_word = Pair.new("direction", "north")
- second_word = Pair.new("verb", "go")
- sentence = [first_word, second_word]
复制代码
這建立了一對 (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 這兩個關鍵字:
- def convert_number(s)
- begin
- Integer(s)
- rescue ArgumentError
- nil
- end
- end
复制代码
你把要試著運行的程式碼放到「begin」的區段裡,再將出錯後要運行的程式碼放到「except」區段裡。在這裡,我們要試著呼叫 Integer() 去處理某個可能是數字的東西,如果中間出了錯,我們就「rescue」這個錯誤,然後返回 「nil」。
在你寫的掃描器裡面,你應該使用這個函數來測試某個東西是不是數字。做完這個檢查,你就可以聲明這個單詞是一個錯誤單詞了。
What You Should Test
這裡是你應該使用的測試檔案 test/test_lexicon.rb:
- require 'test/unit'
- require_relative "../lib/lexicon"
- class LexiconTests < Test::Unit::TestCase
- Pair = Lexicon::Pair
- @@lexicon = Lexicon.new()
- def test_directions()
- assert_equal([Pair.new(:direction, 'north')], @@lexicon.scan("north"))
- result = @@lexicon.scan("north south east")
- assert_equal(result, [Pair.new(:direction, 'north'),
- Pair.new(:direction, 'south'),
- Pair.new(:direction, 'east')])
- end
- def test_verbs()
- assert_equal(@@lexicon.scan("go"), [Pair.new(:verb, 'go')])
- result = @@lexicon.scan("go kill eat")
- assert_equal(result, [Pair.new(:verb, 'go'),
- Pair.new(:verb, 'kill'),
- Pair.new(:verb, 'eat')])
- end
- def test_stops()
- assert_equal(@@lexicon.scan("the"), [Pair.new(:stop, 'the')])
- result = @@lexicon.scan("the in of")
- assert_equal(result, [Pair.new(:stop, 'the'),
- Pair.new(:stop, 'in'),
- Pair.new(:stop, 'of')])
- end
- def test_nouns()
- assert_equal(@@lexicon.scan("bear"), [Pair.new(:noun, 'bear')])
- result = @@lexicon.scan("bear princess")
- assert_equal(result, [Pair.new(:noun, 'bear'),
- Pair.new(:noun, 'princess')])
- end
- def test_numbers()
- assert_equal(@@lexicon.scan("1234"), [Pair.new(:number, 1234)])
- result = @@lexicon.scan("3 91234")
- assert_equal(result, [Pair.new(:number, 3),
- Pair.new(:number, 91234)])
- end
- def test_errors()
- assert_equal(@@lexicon.scan("ASDFADFASDF"), [Pair.new(:error, 'ASDFADFASDF')])
- result = @@lexicon.scan("bear IAS princess")
- assert_equal(result, [Pair.new(:noun, 'bear'),
- Pair.new(:error, 'IAS'),
- Pair.new(:noun, 'princess')])
- end
- end
复制代码
記住你要使用你的專案骨架來建立新專案項目,將這個 Test Case 寫下來(不許複製貼上!),然後編寫你的掃描器,直至所有的測試都能通過。注意細節並確認結果一切工作良好。
設計提示
集中一次實現一個測試,盡量保持簡單,只要把你的 lexicon.rb 詞彙表中所有的單詞放那裡就可以了。不要修改輸入的單詞表,不過你需要建立自己的新列表,裡邊包含你的語彙元組。另外,記得使用 include? 函式來檢查這些語彙陣列,以確認某個單詞是否在你的語彙表中。
加分習題
- 改進單元測試,讓它覆蓋到更多的語彙。
- 向語彙列表添加更多的語彙,並且更新單元測試程式碼。
- 讓你的掃描器能夠識別任意大小寫的詞彙。更新你的單元測試以確認其功能。
- 找出另外一種轉換為數字的方法。
- 我的解決方案用了37 行程式碼,你的是更長還是更短呢?
|
|