更复杂的用户输入
你的游戏可能工作的很好了,但处理用户输入方式肯定让你烦不胜烦。每个房间都需要一套自己的语句,且只有用户输入正确后才能执行。你需要一个设备,它允许用户以各种方式输入短语。如下几种表述都应被支持:
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 = raw_input('> ')
words = stuff.split()
语汇元组
检查句子断成的单词的类型——将用到元组数据结构。元组实际就是不能修改的列表。
first_word = ('direction', 'north')
second_word = ('verb', 'go')
sentence = [first_word, second_word]
这样就创建了一个(TYPE, WORD)组,让你识别出单词,并对它执行指令。
总结:接收用户输入,用split将其分为单词,分析这些单词,识别其类型,最后重新组成一个句子。
扫描输入
写一个扫描器:将用户输入的字符串当做参数,返回有多个(TOKEN, WORD)组成的一个列表,这个列表实现类似句子的功能。若一个单词不在预定的单词语汇表中,那它返回时WORD还在,但TOKEN应该设置成一个专门的错误标记——告诉用户哪里出错了。你需要自己写出来,并通过下面给的单元测试。
异常和数字
数字转换,需要用‘异常’来做,异常指运行某个函数得到的错误。你需要去‘处理’这个异常。
>>> int('hell')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'hell'
ValueError就是int()函数抛出的一个异常,你只需处理这个异常就行了:
def convert_number(s):
try:
return int(s)
except ValueError:
return None
把试着运行的代码放到try块里,再将出错后要运行的代码放到except块里。上面代码就是试着调用int()去处理某个可能是数字的东西,若中间出错,就抓到这个错误,然后返回None。
在你写的扫描器中,应使用上面这个函数来测试某个东西是不是数字。做完这个检查,就可以声明这个单词是一个错误单词。
应该测试的东西
测试文件tests/lexicon_tests.py
from nose.tools import *
from ex48 import lexicon
def test_directions():assert_equal(lexicon.scan("north"), [('direction', 'north')])
result = lexicon.scan("north south east")
assert_equal(result, [('direction', 'north'),
('direction', 'south'),
('direction', 'east')])
def test_verbs():assert_equal(lexicon.scan("go"), [('verb', 'go')])
result = lexicon.scan("go kill eat")
assert_equal(result, [('verb', 'go'),
('verb', 'kill'),
('verb', 'eat')])
def test_stops():assert_equal(lexicon.scan("the"), [('stop', 'the')])
result = lexicon.scan("the in of")
assert_equal(result, [('stop', 'the'),
('stop', 'in'),
('stop', 'of')])
def test_nouns():assert_equal(lexicon.scan("bear"), [('noun', 'bear')])
result = lexicon.scan("bear princess")
assert_equal(result, [('noun', 'bear')
('noun', 'princess')])
def test_numbers():assert_equal(lexicon.scan("1234"), [('number', 1234)])
result = lexicon.scan("3 91234")
assert_equal(result, [('number', 3),
('number', 91234)])
def test_errors():assert_equal(lexicon.scan("ASDFADFASDF"), [('error', 'ASDFADFASDF')])
result = lexicon.scan("bear IAS princess")
assert_equal(result, [('noun', 'bear'),
('noun', 'IAS'),
('noun', 'princess')])
记住要使用你的项目骨架来创建新项目,将此测试用例写下来,然后编写你的扫描器,直至所有的测试都能通过。注意细节并确认一切工作良好。
设计提示
集中一次实现一个测试项目,尽量保持项目简单,只要把你的lexicon.py模块中的语汇表的所有单词放那里就可以了。不要修改输入的单词列表,但要创建自己的新列表,里边包含你的语汇元组。记得使用in关键字来检查这些语汇列表,以确认某个单词是否在你的语汇表中。在你的解决方案中使用目录。
附加练习
1. 改进单元测试,让它覆盖到更多词汇
2. 向语汇列表添加更多的单词,并更新单元测试代码
3. 确认你的扫描器能识别任意大小写的单词。更新单元测试以确认它实际工作
4. 找出另一种转换数字的方法
5. 我的解决方案用了37行代码,你的呢?
常见问题回答
为什么我老看到ImportError?
通常四种错误导致ImportError:在模块路径下没有创建__init__.py;在错误路径下执行了import;拼写错误,导致导入错误模块;没有设置到PYTHONPATH, 所以无法从当前路径加载模块
try-except和if-else有什么不同?
try-except仅用于处理异常,绝不要将其作为if-else使用
有没有办法让游戏在等待用户输入的时候不间断的运行?
我猜你想将游戏做的更高级,用户反应过慢就被杀死之类的。这个可以有,但需要用到更高级模块和编程技巧,本书不涉及。