项目 3:搭档 A 练习
Project 3 中搭档 A 的第一部分练习(A3–A5):进出过程、说话行为、上锁地点、车库与停车票。
第 A3 题
你会发现,每当一个人物去一个新地方,那个地方就会收到一条 enter 消息。此外,人物之前所在的地方会收到一条 exit 消息。地方收到消息时,会依次调用它的 entry-procedures 或 exit-procedures 列表上的每个过程。地方有以下几个方法来操作这些过程列表:add-entry-procedure、add-exit-procedure、remove-entry-procedure、remove-exit-procedure 和 clear-all-procs。你可以在代码里读到它们的定义。
Sproul Hall 身上挂着一个特别烦人的 exit 过程。请修改 sproul-hall-exit,让它统计自己被调用的次数,第三次之后就停止烦人。
记住,exit-procs 列表里装的是过程,不是过程的名字!仅仅重新定义 sproul-hall-exit 是不够的,因为 Sproul Hall 的 exit 过程列表里仍然装着旧的那个过程。最好的办法是重新加载一遍 adv-world.scm,这会定义一个新的 Sproul Hall,并添加新的 exit 过程。
第 A4 题:第一部分
我们为人物提供了通过消息 talk 和 set-talk 说话的能力。你可能已经注意到,校园里有些人一有人路过就开始搭话。我们想模拟这种行为。在任何这样的交互中,涉及两个人:一个本来就在这个地方的人(下称 talker),另一个是刚进这个地方的人(即 listener)。我们已经提供了这样的机制:listener 进入地方时会向地方发一条 enter 消息。另外,每个人物都能接受一条 notice 消息,意思是该人物应当注意到有新的人来了。talker 会收到一条 notice 消息,然后开口说话,因为我们已经让人物的 notice 方法给自己发一条 talk 消息。(后面我们会看到,某些特殊种类的人物有不同的 notice 方法。)
你的任务是修改地方的 enter 方法,让它在原有功能之外,再给该地方的每个人物发一条 notice 消息,但刚进来的那个人除外。notice 消息应当把新进来的人物作为参数。(现在你还用不到这个参数,但以后会需要。)
把下面这些加到 adv-world.scm 里:
(define singer (instantiate person 'rick sproul-plaza))
(ask singer 'set-talk "My funny valentine, sweet comic valentine")
(define preacher (instantiate person 'preacher sproul-plaza))
(ask preacher 'set-talk "Praise the Lord")
(define street-person (instantiate person 'harry telegraph-ave))
(ask street-person 'set-talk "Brother, can you spare a buck")
试着走到 sproul-plaza 和 telegraph-ave 附近,看看这些消息是否被触发。
你必须提交一份操作记录,展示你的角色四处走动并触发这些消息的过程。
第 A4 题:第二部分
到目前为止,程序假设谁都可以去任何地方。而在现实生活中,很多地方都有锁着的门。
请为地方发明一条 may-enter? 消息,它接受一个人物作为参数,并且总是返回 #t。然后发明一个 locked-place 类,它的 may-enter? 方法在地点未上锁时返回 #t,上锁时返回 #f。(它初始状态应当是上锁的。)locked-place 类还必须有一条 unlock 消息。为简化起见,把这个方法写成无参数形式,并让它总是成功。在真正的游戏里,我们还会发明钥匙,以及一套要求人物持有正确钥匙才能开门的机制。(这正是 may-enter? 要把人物作为参数的原因。)
修改 person 类,让它在从一个地方移动到另一个地方之前检查进入许可。如果人物不能进入,返回一个错误。然后创建一个上锁的地点并测试它。
注意: locked-place 应当有一个实例化变量,即它的名字。
(define warehouse (instantiate locked-place 'warehouse))
第 A5 题
到处走走固然好,但有些人住得远,需要把车停在车库里。交通工具只是一种 thing,但你要发明一种特殊的地点,叫做 garage。车库有两个方法(所有地点共有的方法之外):park 和 unpark。你还需要一种特殊的 thing,叫做 ticket;它的特殊之处在于有一个 number 作为实例化变量。
park 方法接受一个交通工具(一种 thing)作为参数。首先要确认这个交通工具确实在车库里。(拥有交通工具的人会先进车库,然后要求停车,所以发出 park 消息之前,交通工具应当已经随人物一起进了车库。)然后生成一张有唯一序列号的 ticket。(序列号计数器应当在所有车库之间共享,以免以后想从另一个车库取车时惹麻烦。)每张票的名字都应当是 ticket。
你要把票号和交通工具关联起来,存进一个键值表,就像第 6 课里 get 和 put 用的那种。不过,get 和 put 针对的是所有操作共用的一个固定表;而这里我们需要每个车库各有一张表。文件 tables.scm 里提供了表抽象数据类型的实现:
构造函数:(make-table) 返回一个新的空表。
赋值函数:(insert! key value table) 向表中添加一组键值对。
选择函数:(lookup key table) 返回对应的值,若表中没有该键则返回 #f。
表的实现原理见 SICP 3.3.3(第 266–268 页)。目前先把它当作现成的基本工具。
请在表里建一条记录,以票号为键、交通工具为值。然后让票的所有者丢掉这个交通工具,并拿走票。
unpark 方法接受一张票作为参数。首先要确认你拿到的对象确实是票(通过检查名字)。然后在车库的表里查这个票号。如果找到交通工具,就让票的所有者丢掉票并拿走交通工具。同时,在表里把这个票号对应的值改成 #f,这样人们就不能把同一辆车取两次。
现实中的车库容量有限,而且停车要收费;但为了简化项目,这些方面你都不用模拟。
切勿把任何东西命名为 “car”!这会把一切搞乱!
注意:
- 一张票只有一个实例化变量,即序列号。(例如
(instantiate ticket 120)。) - 一张票是一种
thing,名字为'ticket - 一个车库有一个实例化变量,即它的名字。(例如
(instantiate garage 'soda-garage)。) - 不要为交通工具定义新的类。你可以假设
park被调用时参数是正确的。 - 给无主的交通工具停车应当返回一个错误。
- 取一辆没停在这的车应当返回一个错误。