LearningZIL笔记

第1章 基本

大致上IF由两类实体构成, 对象和例程. 对象又大致上可分为两类, 房间和物品.

既然处理了输入, 游戏就必须在适当的时候产生回复.

parser的目的在于将用户的输入规约为动词, 直接宾语, 间接宾语. 在ZIL中, 它们分别被称为PRSA, PRSO, PRSI. parser的结果分为三类, 仅包含动词, 包含动词和直接宾语, 包含动词, 直接宾语和间接宾语.

间接宾语将首先拥有处理输入的机会, 其次是直接宾语, 最后是动词. 这种做法的想法在于, 它们的特殊性依次降低.

第2章 创建房间

以下是一个房间的定义:

<ROOM LIVING-ROOM
  (LOC ROOMS)
  (DESC "Living Room")
  (EAST TO KITCHEN)
  (WEST TO STRANGE-PASSAGE IF CYCLOPS-FLED ELSE
        "The wooden door is nailed shut.")
  (DOWN PER TRAP-DOOR-EXIT)
  (ACTION LIVING-ROOM-F)
  (FLAGS RLANDBIT ONBIT SACREDBIT)
  (GLOBAL STAIRS)
  (THINGS <> NAILS NAILS-PSEUDO)>

ROOM代表这是一个房间对象的定义. LIVING-ROOM是这个对象的名字. LOC一行给出了对象的位置, 所有的房间都位于一个特别的对象之中, 其被称为ROOMS. DESC一行也给出了某种名字, 这样的名字不是供内部引用的, 而是供玩家看到的.

接下来数行给出了房间的出口. 许多房间的定义中, 出口占据了多数内容. 出口分为不同的种类, 例如(EAST TO KITCHEN)定义了一个无条件出口 (UEXIT). 第五行和第六行定义了一个有条件出口 (CEXIT), 其涉及一个全局变量CYCLOPS-FLED, 要么为真要么为假. 第七行是函数出口 (FEXIT) 的一个例子, PER标示了其为函数出口. 例程TRAP-DOOR-EXIT将决定玩家是否能够移动, 移动到哪里, 以及若不能移动该怎样回应. 还有两种出口, 虽然该房间的定义没有用到. 一种是非出口 (NEXIT), 用SORRY标示, 例如

(NW SORRY "The soldier at Uncle Otto's front door
informs you that only Emperor Bonaparte is allowed
through.")
不论如何玩家都不能走这个方向. 另一种是门出口 (DEXIT), 它与有条件出口类似, 只是把全局变量换成了一个门对象, 例如
(SOUTH TO GARAGE IF GARAGE-DOOR IS OPEN ELSE
 "You ought to use the garage door opener.")
注意到有条件出口里是没有IS OPEN的.

ACTION一行是房间的动作例程, 这之后再说. FLAGS一行包含了可应用于该房间的FLAG. RLANDBIT意味着房间在陆地上, 而不是在水上, 或者空中. ONBIT意味着该房间永远是亮着的. 有的FLAG每个游戏都有, 有的则是特定于游戏的. 例如, SACREDBIT特定于ZORK I, 它意味着神偷不会光顾这个房间. 根据命名约定, 所有的FLAG都以BIT结尾, 你可以根据自己的需要使用特殊的FLAG.

第3章 创建物品

以下是一个物品定义:

<OBJECT LANTERN
  (LOC LIVING-ROOM)
  (SYNONYM LAMP LANTERN LIGHT)
  (ADJECTIVE BRASS)
  (DESC "brass lantern")
  (FLAGS TAKEBIT LIGHTBIT)
  (ACTION LANTERN-F)
  (FDESC "A battery-powered lantern is on the trophy case.")
  (LDESC "There is a brass lantern (battery-powered) here.")
  (SIZE 15)>

(LOC LIVING-ROOM)代表铜灯最初在起居室里. 随着游戏进程的推进, 其位置也有可能发生变化. 例如, 若玩家捡起铜灯, 那么它的LOC就会变为PLAYER对象, 有时PLAYER也被称为PROTAGONIST. 之后若玩家在厨房丢掉铜灯, 铜灯的LOC就是厨房了.

SYNONYM性质是一些可以用来引用铜灯的名字. ADJECTIVE性质是一些用来限定引用物品的形容词, 该性质是可选的.

DESC性质自然是物品的名字, 它会出现在例程需要插入名字的时候.

铜灯具有两个FLAG, TAKEBIT意味着它可以被玩家捡起, LIGHTBIT意味着它可以被点亮. 铜灯现在不是开着的, 一旦它被打开, 其就拥有了ONBIT, 意味着它正在发着光.

ACTION性质将LANTERN-F作为处理与铜灯相关的输入的动作例程. 例如, 若玩家输入THROW THE NERF BALL AT THE BRASS LANTERN, 那么LANTERN对象就是PRSI, 而例程LANTERN-F将首先拥有处理输入的机会. 如果玩家输入了THROW THE BRASS LANTERN AT THE NERF BALL, 那么LANTERN将会是PRSO. 当nerf ball的动作例程不能处理输入的时候, LANTERN-F也将拥有处理输入的机会.

FDESC性质是一个字符串, 当玩家第一次捡起铜灯的时候, 它将被用来描述铜灯. LDESC是当铜灯在地上的时候用来描述铜灯的字符串. 如果一个物品没有FDESCLDESC, 那么DESC性质就会被用来描述对象.

SIZE属性定义了物品的大小/重量. 这帮助游戏判断玩家是否可以捡起某个东西, 还是说已经拿得太多了. 如果不给可拿的物品提供SIZE, 通常默认的情况是5.

第4章 ZIL中的例程

例程的基本形式如下:

<ROUTINE ROUTINE-NAME (argument-list)
  <guts of the routine>>

命名例程有许多约定, 例如通常物品和房间的动作例程的名字以-F结尾. 参数列表跟在例程的名字之后出现. 参数是只在特定例程中被使用的变量, 这有别于全局变量. 以下是两个简单的例程:

<ROUTINE TURN-OFF-HOUSE-LIGHTS ()
  <FCLEAR ,LIVING-ROOM ,ONBIT>
  <FCLEAR ,DINING-ROOM ,ONBIT>
  <FCLEAR ,KITCHEN ,ONBIT>>
<ROUTINE INCREMENT-SCORE (NUM)
  <SETG SCORE <+ ,SCORE .NUM>>
  <COND (,SCORE-NOTIFICATION-ON
         <TELL "[Your score has just gone up by "
               N .NUM ".]" CR>)>>
调用例程的时候, 例程的名字写在开头, 参数跟在后面, 用尖括号括起来.

除了正常的参数之外, 还有两类参数, 可选参数和辅助参数, 分别以"OPT""AUX"标示. 描述例程的参数时, 必须按照正常参数, 可选参数, 辅助参数的顺序. 以下是一个例子.

<ROUTINE RHYME ("AUX" ARG1 ARG2)
  <SET ARG1 30>
  <SET ARG2 "September">
  <LINE-IN-RHYME .ARG1 .ARG2>
  <SET ARG1 28>
  <SET ARG2 "February">
  <LINE-IN-RHYME .ARG1 .ARG2>
  etc.>
<ROUTINE LINE-IN-RHYME (ARG-A ARG-B)
  <TELL N .ARG-A " days hath " .ARG-B "." CR>>
调用RHYME的时候, 是不需要参数的, 因为ARG1ARG2都是辅助参数. 再看一个例子.
<ROUTINE CALLEE (X "OPT" Y "AUX" Z)
  <some-stuff>>
你有两种调用CALLEE的方式, 例如<CALLEE .FOO>或者<CALLEE .FOO .BAR>.

编写例程的时候, 你总是需要用到条件表达式COND, 以下是其一般格式:

<COND (<predicate-1>
       <do-stuff-1>)
      (<predicate-2>
       <do-stuff-2>)
      (<predicate-3>
       <do-stuff-3>)>
其行为就和其他Lisp方言里的差不多.

在正常情况下, 调用一个例程返回其最后一个表达式的值, 不过读者也可以通过<RTRUE><RFALSE>提前返回真或假, 或者通过RETURN提前返回其他什么值.

ZIL代码中有些东西看起来像例程, 不过你在游戏程序的任何地方都找不到, 这些东西被称为ZIL指令. ZIL指令是游戏与运行在微机上的解释器交流的手段, 有时其也被称为操作码 (op-code). 关于当前ZIL指令的完整列表, 请参考附录D.

第5章 简单动作例程

假设现在你有了一个对象AVOCADO, 带有性质(ACTION AVOCADO-F), 那么AVOCADO-F就是AVOCADO的动作例程了, 它长得可能像以下这样:

<ROUTINE AVOCADO-F ()
  <COND (<VERB? EAT>
         <REMOVE ,AVOCADO>
         <TELL "The avocado is so delicious that you
eat it all." CR>)
        (<VERB? CUT OPEN>
         <FSET ,AVOCADO ,OPENBIT>
         <MOVE ,AVOCADO-PIT ,AVOCADO>
         <TELL "You halve the avocado, revealing a
gnarly pit." CR>)>>
(VERB? EAT)为真当且仅当PRSAEAT. 若的确如此, 那么先REMOVE这个鳄梨, 即将其LOC设置为假, 然后调用TELL输出想要告诉玩家的信息. 如果EAT不是动词, 而CUT或者OPEN是动词的话, 那么就会先调用FSET, 它的意思是"flag set", 将为AVOCADO添加OPENBITflag. 如果你想要去除某个flag, 那就调用FCLEAR. 接着, 还是调用TELL告诉玩家发生了什么. 若动词不是以上三个, 那么AVOCADO-F就没能成功处理输入. 如果AVOCADOPRSI, 那么接下来就由PRSO处理输入. 如果AVOCADOPRSO, 那么接下来就由动词处理输入.

除了VERB?, 还有其他许多常用的谓词, 例如EQUAL?, 请看例子:

<EQUAL? ,HERE ,DRAGONS-LAIR>
HERE是一个全局变量, 其总是被设置为当前的房间. 引用全局变量的值时, 前面要加上逗号. 引用局部变量的值时, 前面要加上点号. 许多全局变量只是取布尔值而已, 因此它们本身就是谓词. 实际上, EQUAL?可以接受多于两个参数.

FSET?判断一个对象是否拥有某个flag, 例如

<FSET? ,AVOCADO ,OPENBIT>

IN?可以用来检查一个对象的位置, 例如

<IN? ,EGGS ,BASKET>

AND, OR, NOT表现得就如同其他Lisp方言.

房间的动作例程不是用来直接处理玩家的输入的. 一般而言, 房间的动作例程会接受一个参数RARG, 代表room argument, 房间参数之意. 通常的RARGM-LOOKM-ENTER. 许多时候, 处理M-ENTER时, 房间的动作例程会做些玩家不可见的事情.

第6章 事件

不是所有的文本都为了回应玩家的输入, 有些也可能是事件的结果, 事件也被称为中断. 中断的命名约定是在前面加上I-, 例如I-GUNSHOT, 以下是一个简单中断的例子:

<ROUTINE I-OTTO-GOES-NUTS ()
  <FSET ,UNCLE-OTTO ,LOONEYBIT>
  <COND (<IN? ,UNCLE-OTTO ,HERE>
         <TELL
"Sigh; it appears that Uncle Otto's delusion has
returned; he has begun shouting orders to unseen
troops." CR>)>>

在绝大多数回合里, 时间会在故事之中流逝, 例外可能有parser没能解析玩家的输入之类的情况. 在每个回合的结尾, 已经处理好了玩家的输入, 时间也被推进了, 此时一个被称为CLOCKER的例程会被调用. 它会为被安排上的中断例程倒计时, 并调用到点了的中断.

任何进行了TELL的中断应该返回真, 否则应该返回假. 这是为了动词WAIT的方便, 其作用是忽略数个回合. 中断必须能够终止WAIT, 以防以下事情发生:

>WAIT
Time passes ...
    A truck begins speeding toward you.
    The truck loudly honks its horn.
    Since you refuse to move out of the way, the truck
merges you into the pavement.

中断是由作者加入的. 有可能在游戏的开头, 或许是为了响应玩家的某个动作. 一个中断例程或许也会加入其他的中断例程. 调用QUEUE加入例程, 它有两个参数, 一个是中断, 另一个是倒计时回合数:

<QUEUE I-SHOOTING-STAR 10>
倒计时为1的, 本回合就会开始运行. 倒计时为2的, 那就是下回合, 依此类推. 一般情况下, 一个中断只会运行一次, 除非你再次加入它. 例外情况在于, 如果倒计时为-1, 那么每回合都会运行, 除非你主动DEQUEUE它.

倒计时为-1的一个例子是之前看到的卡车的中断I-TRUCK, 它与一个被初始化为0的全局变量TRUCK-COUNTER协同运作:

<ROUTINE I-TRUCK ()
  <SETG TRUCK-COUNTER <+ ,TRUCK-COUNTER 1>>
  <COND (<EQUAL? ,TRUCK-COUNTER 1>
         <MOVE ,TRUCK ,STREET>
         <TELL
"A truck begins speeding toward you." CR>)
        (<EQUAL? ,TRUCK-COUNTER 2>
         <TELL
"The truck loudly honks its horn." CR>)
        (<EQUAL? ,TRUCK-COUNTER 3>
         <COND (<EQUAL? ,HERE ,STREET>
                <JIGS-UP
"Since you refuse to move out of the way, the truck
merges you into the pavement.">)
               (T ;"you've gotten out of the way"
                <TELL
"The truck blasts you with hot exhaust fumes as it
rumbles past." CR>)>)
        (T ;"counter is 4"
         <DEQUEUE I-TRUCK>
         <SETG TRUCK-COUNTER 0>
         <TELL
"The truck vanishes in the direction of Hoboken."
CR>)>>
分号后面是注释. 例程体的第一行是在计数, 如此I-TRUCK才知道卡车开了多远. 例程JIGS-UP以一个字符串为参数, "杀死"玩家, 输出接受的字符串, 最后再提示一下玩家已经死了. 这里只有当玩家还在STREET的时候才会调用JIGS-UP, 否则的话它就会提示卡车(安全地)过去了. 当TRUCK-COUNTER4时, 也就是对应谓词为T的分支. 此时I-TRUCK将被DEQUEUE, 并且注意一下TRUCK-COUNTER也被设置为0, 如此未来再次运转I-TRUCK时也能表现正确.

在回合末, 但在CLOCKER之前, 当前房间的动作例程将被自动地调用以参数M-END. 以下是一个例子:

<ROUTINE AIRPORT-F (RARG)
  <COND (<EQUAL? .RARG ,M-ENTER>
         <QUEUE I-STRAFING 5>)
        (<EQUAL? .RARG ,M-LOOK>
         <TELL
"You are on the tarmac of an airport runway..." CR>)
        (<EQUAL? .RARG ,M-END>
         <TELL "A plane zooms low overhead." CR>)>>

第7章 更多关于ZIL代码的说明

GLOBAL形式定义全局变量, 例如:

<GLOBAL SECRET-PASSAGE-OPENED <>>
<GLOBAL SLEEPY T>
<GLOBAL NUMBER-OF-MATCHES 5>

容器系统是ZIL的重要组成部分. 每个对象都有LOC, 房间位于特殊的对象ROOMS之中. 有的对象的LOC可能为假. 容器系统决定了许多重要的事情. 例如, 它决定一个对象是否可被引用. 一般情况下, 能够被引用的对象必须在场并且是可见的. 例如, 如果一个东西不在玩家所处的房间, 或者它处于一个封闭容器之中, 或者它本身拥有一个INVISIBLEflag, 那么它就没法被引用. 为了得到一个对象的LOC, 只需要使用LOC:

<LOC ,OBJECT-NAME>
你可以使用IN?来检查对象的位置:
<IN? ,PICKLE ,BARREL>
MOVE以改变对象的位置:
<MOVE ,HORSE ,STABLE>
REMOVE以去除对象:
<REMOVE ,HORSE>
为了探查一个对象的内容, 你需要两种指令, FIRST?NEXT?. 假设你现在有了一个叫做KITCHEN-CABINET的对象, 它包含一个pitcher, 一个serving spoon, 还有一个severed head.
<FIRST? ,KITCHEN-CABINET>
将返回对象PITCHER, 然后
<NEXT? ,PITCHER>
将会是serving spoon, 它的NEXT?又会是severed head. 既然severed head是cabinet所包含的最后一个东西, 那么
<NEXT? ,SEVERED-HEAD>
根据定义应该是假.

机动装置:

第8章 更大的图景

当玩家启动游戏的时候, 解释器第一件所做的事情是调用被称为GO的例程, 它应该完全所有的准备工作, 例如调用V-VERSION, 调用V-LOOK, 加入必要的中断, 等等. GO应该做的最后一件事情是调用被称为MAIN-LOOP的例程, 它在某种意义上是整个游戏的king.

<ROUTINE MAIN-LOOP (argument-list-from-hell)
  <REPEAT ()
    <PARSER>
    <COND (<did-the-parser-fail?>
           <AGAIN>)>
    <PERFORM ,PRSA ,PRSO ,PRSI>
    <COND (<did-this-input-cause-time-to-pass?>
           <call-room-function-with-M-END>
           <CLOCKER>)>>>

第9章 句法文件

第10章 演员

第11章 描述过程

第12章 一些复杂的东西

第13章 图形和声音

第14章 组织你的ZIL文件

第15章 庆祝时刻—编译你的游戏

第16章 使用ZIL编写其他类型的游戏

附录A: 性质

附录B: flag

附录C: 常用例程

附录D: ZIL指令