Home Map Index Search News Archives Links About LF
[Top bar]
[Bottom bar]
Philipp Gühring
作者 Philipp Gühring

关于作者:
Philipp 在 HTL Wiener Neustadt 通过了 A 级考试。该学校是一所电子数据处理(EDP)方面的高等工科学院。现在他正专注于他的Futureware 2001软件开发组。同时,他还是一个 Linux 迷和一个活跃的 Linux User Group Austria 成员。

目录:

面向会话的编程语言——Dialog

Josi, one of the customers

摘要:

Dialog 是一个面向会话的编程语言。用户可以通过它来编写自己的会话内容。它被用在商业仿真软件 Würstelstand 的开发上。本文旨在介绍如何使用 Dialog 语言来开发会话程序。



 

引言

用德文编写的 Würstelstand 是澳大利亚的一个商业仿真软件。使用者将试着管理一个热狗卖点。通过与客户的接触使用者近乎置身于一个冒险游戏之中。为了完善这个客户-接触功能我开发了一个会话语言。它将满足以下要求:

当时在编写电话会话过程中,我再一次使用了 dialog 引擎并增强了它的功能。玩家将使用一个多重选择系统来控制里面的角色 Leni——一个热狗摊摊主。顾客由计算机来模拟生成。玩家的目标是为顾客提出建议,和他们谈话并卖给他们东西。顾客在他们饥饿并且有空闲时间的时候自动出现。此外,玩家也可以主动的给顾客打问询电话。


 

Dialog 语言

会话以 Ascii 文本文件的方式储存并被逐行解释。当然这种方式可以改进成任选行方式。我们通常使用简单的文本编辑器来创建这些文件。文件名形如 Name.BAT,例如 HALE.BAT。玩家(Leni)自动说出如下这些话:

Leni: Text
Leni: Good morning, Sir! 
      What can I get you?
Leni: Look at these youngsters!
      Unbelievable!
Leni is talking
看看现在这些年轻人!真不敢相信。你刚从床上掉下来了吗?
(Look at these youngsters! Unbelievable! Did you
fell out of the bed?)

Customer is talking
一块面包、两根法兰克福香肠外加两杯可乐。快点,老家伙!
(Two Frankfurter with bread and two Coke. Come on, get going, old man!)

对方(顾客)说:(Kunde 在德语中是“顾客”的意思而 Telefone 是“电话”的意思)
Kunde: Text
Kunde: Two Frankfurter with bread and Coke. 
       Come on, get going, old man!
Telefon: Futureware 2001, Philipp Gühring.
         What can I do for you?

所有的会话都以

Ende

正常结束(Ende 在德语中就是结束的意思)。

一个简单的示例:
Leni: Good morning, Sir! What would you like?
Kunde: Good day! A Käsekrainer please!
Leni: Just a moment.
Leni: Here you are.
Kunde: Thanks a lot. Bye!
Leni: Bye!
Ende

跳转目标由放在行首的冒号(:)定义,其后紧跟跳转目标的名字。用户可以通过命令 Sprung 跳转到指定的目标处(Sprung 在德语中是跳转的意思):

:Target
//Jump example follows:
Sprung Target

示例:
...
Leni: 1
//First we do this
SPRUNG MENU_0
//I'll be back!
...
//These commands are not executed
Leni: 2
:MENU_0
//I am back!
Leni: 3

这个例子将告诉解释程序做些什么呢?首先,解释程序会发现命令 Leni: 随之输出 text 1。然后它会忽略下一行(//First we do this),因为该行是由双斜杠(//)开始的注释行。接下来的一行中含有命令 Sprung。解释程序将在整个会话中搜索目标 MENU_0。找到后则跳转到该处。随后还是一个注释行(I am back!)。最后一个命令是 Leni:,其结果是在屏幕上输出 3。从示例的解释过程来看,第二个命令 Leni: 2 被跳过去了,也就是 Leni 不会说 2

如上所见:一个会话行可以:

注释行可以由 ;(分号),//(双斜杠), (空格)和 *(星号)引导。这些注释符号增强了会话文件的可读性。因此解释程序将忽略它们,例如:
// This is a comment
**************************************
*Like this, one can make comments too*
**************************************
注释一定不要和命令共处一行:
Leni: I don't unterstand nothing anymore.  // NO COMMENT

如上的命令解释程序将会把 I don't unterstand nothing anymore. // NO COMMENT 作为文本输出。 

多重选择系统

Multiple-Choice
  • What do you do for work?
  • Do you like my Würstelstand?
  • What can I get you?

Dialog 发明了下面这个系统:该系统提供了一个列表,列表中插入的菜单项是用户可能做出的回答。到某一指定的时间,菜单将显示在屏幕上。这时用户就可以做选择了。解释程序会根据选中的菜单项跳转到相应的程序段。

首先你要用命令 NEUALT 来把菜单项插入到列表中。这两个命令后面都紧跟着跳转目标和菜单项文本。菜单项文本可以很长,系统会自动的截取前面一定长度文本作为有效文本。MENÜ命令将显示整个列表供用户选择。



这里有如下三种选择方式: 

话题,独立于上下文

这种方式对于讨论话题很有用:
Neu buy,A hot one, as usual, ok?
Neu work,How is it going at work?
Neu language,Are you still attending the language course at WIFI?
Neu family,How is your family doing?
Neu weather,Are you enjoying this weather?
Menü

通过这种选择方式,玩家可以一个接一个的遍历所有菜单项。这些选项是可重复使用的。那些没有被选中的菜单项仍被保存在列表中以供选择。好吧,我们先选中上面例子中的 work 来看看将会有什么发生:

:work
Leni: How is it going at work?
Kunde: Too much to do, as always.
Menü
上面已经提到,只有被选中的菜单项才会消失。因此,菜单中会留下这些选项: 下面是另外两种选择方式: 

选项,依赖于上下文的讲话

Kunde: How many would you like?
Alt some, 10 Pieces
Alt more, 20 Pieces
Alt most, 100 Pieces
Menü

:some
//We continue here, when the user chose 10 pieces

:more
...

:most
...
既然把选中的菜单项依旧保存在列表中没有什么意义,那么在用户做出选择之后所有的菜单项都应该被删除。在这个例子中,当用户选择 20 pieces 项之后,解释程序将跳转到目标 more
:more
Kunde: Are you sure?
Leni: Yes, I want 20 Pieces.
Kunde: How soon do you need them?
Alt 1, Tomorrow
Alt 2, The day after tomorrow
Alt 3, Sometimes
Menü
把上面的两种方式结合在一起就形成了第三种选择方式: 

上下文,话题的改变

这里有一个问题值得我们注意:一旦用户在与他的交谈对象就某个话题讨论过之后却还想说些别的事情,他应该可以选择是改变话题还是把以前的选择付诸实行。如果用户选择改变话题,那么以前做的选择将会失效。因此,需要为用户增加粘贴话题和跳转话题的功能。我们在 Dialog 中采用了如下方法:评论(remark)作为一个标准的选项插入已经含有话题的列表中。用户选中项下面的选项会消失其中那些没有被选中的将保存起来。

Kunde: Remember the good old days.
Alt Memory,Yeah, I just rememberd when, ...
MENÜ
 

执行

也许你很想知道我们是如何把这些不同的概念付诸实现的。其实,到现在为止你已经知道了 NEUALT 在使用中的差别。用 NEU 插入的菜单项将一直保存在列表中直到它被选中;而用 ALT 插入的选项则不管它是否被选中都将自动被删除。 

几个列表

如果用户需要在讨论的同时做出选择而不希望在菜单种显示出其它话题,我们该如何做呢?基于这个目的,我开发了如下三个列表:

列表 0 最好用于选项。列表 1 适用于普通话题,如家庭、工作、休闲、饮食等。如果用户希望讨论的话题中还含有其它的子话题,那么请选择列表 2。我们根据 Hale 的会话做了一个示例。如果用户需要除这三个以外更多的列表可以修改解释程序源代码中的常数值。 

如何使用不同的列表?

会话以列表 1 做为开始时的当前列表。玩家可以用命令 LISTE 来改变当前列表。
LISTE 0
LISTE 1
LISTE 2
显而易见,在这个过程中所有列表的全部菜单项都将原封不动的保存着。这个命令同 NEU,ALT,MENÜLÖSCHEN 一样与当前列表有关。 

旧版本的 Dialog

在为 Würstelstand 编写的旧版 Dialog 中选择系统的工作机制有点不同:为了实现命令 ALT 的功能我们在命令 NEU 的逗号后增加了一段内容:
Neu Memory,eah, I just rememberd when, ...
列表 0 中的内容在被选择之后将自动删除。所以这个功能只对选项有用,而不能用于话题。

我建议读者看看示例 HALE.BAT 和 PETER.BAT,那里面的列表用的很不错。

LÖSCHEN target
删除当前列表中指向目标 target 的所有菜单项。例如:
LÖSCHEN familiy
为了删除当前列表中的所有菜单项,用户可以在该命令后跟星号:
LÖSCHEN *
(如果需要,用户可以增加对规则表达式的支持;-)

Menü 命令显示当前列表的所用菜单项并允许用户在其间做出选择。之后选中项以及所有附加选项(用 ALT 插入的项)将被从列表中删除。最后解释程序跳转到目标指定处。假设列表中只有一个菜单项,那么这个菜单项将被认为是选项也因此不会出现多重选项的菜单。如果列表中没有菜单项或者跳转目标无效,解释程序将顺序执行 MENÜ 的先一行。 

界面

会话是如何作用和影响它的环境的呢?会话之间是如何交换数据的呢?下面的内容将会加以介绍。 

存储/寄存器

在 Würstelstand 中,每个会话有 256 个寄存器。每个寄存器保存的数字范围是从 -2*10E9 到 +2*10E9。这 256 个寄存器被分成如下三个部分:

系统寄存器:

前 100 个寄存器(从 0 到 99)为系统保留:它们在会话开始前由系统载入数值。因此会话可以反作用于它的环境。标注 //S 的寄存器在会话结束之后将被重新分析和使用。会话可以通过改变这些寄存器的值来影响环境。下面是 Würstelstand 中的系统寄存器一览表:

1 Event;   //事件数(参见 texte.h)
2 geliefert; //S //0-10,交付几个十分之一
3 wtag;   //星期
4 tag;   //日期
5 monat;   //月份
6 jahr;   //年份
7 Datum;   //使用天数(1.1.1997 = 0)
8 wetter;   //今日天气
9 konto; //S //银行账目
10 kapital; //S //现金
11 ausgaben; //S //今日支出
12 einnahmen; //S //今日收入
13 sterne; //S //热狗卖点的质量等级(0-5 个星号)
14 wverkauf;   //本周售出产品数量
15 weinnahmen;   //周收入
16 wausgaben;   //周支出
17 0; //S //新的收入/支出(由会话触发)
18 Nachrichtenserie;   //新闻系列(news series)(0=Elch,1=...)
19 Nachricht;   //当前新闻系列中的那个新闻(0=1.Tag,1=2...)
20 LottoNr[0];   //使用多少个彩票号码(0-6)
21 LottoErgebnis[0];   //多少个中奖的彩票号码
22 LottoGewinn[LottoErgebnis[0]];   //Leni 盈利多少
23 S.Image; //S //Leni 的肖像
24 S.Override; //S //忽略的事件
25 S.wverkauf[1];   //上周售出产品
26 S.weinnahmen[1];   //上周收入
27 S.wausgaben[1];   //上周支出
28 S.wverkauf[2];   //两周前售出产品
29 S.weinnahmen[2];   //两周前收入
30 S.wausgaben[2];   //两周前支出
31 S.NOverride; //S //明天忽略事件
32 S.wetter_bericht;   //那一个天气预报
33 Gesamtwert();   //热狗卖点的总数
34 Wetterbericht[S.wetter_bericht].Ereignis;   //那一个天气事件
35 Tageszeit;   //白天时间(分)
70..79 Lagermenge   //股票
80..89 Verkaufspreis //S //产品价格
90..99 Kaufmenge //S //订单数量

会话寄存器

接下来的 100 个寄存器(从 100 到 199)为每个会话所私有。在游戏开始时它们被初始化成零然后在整个游戏中一直为会话所有。在保存游戏时它们将被保存。其它操作亦然。只有相应的会话才可以对它们访问。系统及其它会话无权对它们读/写。玩家应该在会话开头对将要使用的会话变量给出必要的说明。

batch.cpp
// Customer: Peter Hinzing 
// 
// Usage of the registers
//[100] How often he was here 
//[101] Pocket money
//[102] Several events
//[103] Random number: order
//[104] Random number: answer to order
//[105] Different dialogs: Work on the 5th day
//[106] Deal
//[107] The game starts, after having been chosen
//[108] Game.stake.type
//[109] Game.stake.quantity
//[110] Game.choose.Peter
//[111] Game.choose.Leni
//[112] Activation of the Hobby 
//[113] Activation of the Home
//[114] Dialog about Würstelstand 
//[115] total stock coke
//[116] too much ?*************************
//* not yet done
在寄存器 [100] 中 Peter 记下他多久访问一次热狗卖点。在他第一次光临时,他要做自我介绍。第十次,他就会变得随便些。在 [101] 里存放这他口袋里的钞票数。如此类推。

共享寄存器

余下的 56 个寄存器(也可能更多)是所有会话共享的寄存器。也就是说:这些寄存器对于所有会话来说都是一样的,所有的会话都有权访问它们。因此必须有一个控制中心来管理这些寄存器的使用。下面三个寄存器被用在 Würstelstand 的会话中(见 daten.h 中的说明)。

[200]: Leni can go to the immigration office with Hale
[201]: Leni read the dog's wanted circular
[202]: Leni had played Stein-Schere-Papier with Peter! (evil!)
 

事件

我们为 Würstelstand 开发了一个事件系统。每个事件都有一个唯一的编号。这些编号由主控文件来协调。事件可以触发下面这些事情:

如何实现这些功能呢?
对于产品、顾客、电话会话以及新闻系列来说,用户必须把事件的编号作为开始/结束值插入相应的数据文件中。

如何触发事件?

Aktion expression
// 启动作弊模式:
Aktion 3
// 激活寄存器 100 中保存值所对应的事件:
Aktion [100]
如何正确的使用这些事件呢?这里有一个关于 Würstelstand 中部分事件的列表:
0 Error/Never 事件 0 被保留
1 Initialising 在程序开始是被触发并激活许多产品、顾客等
2 End 在游戏结束时触发
3 activating FW-Cheat 谁编写的这些代码?!?
4 deactivating FW-Cheat 保存这个秘密!
5 Leni.competition.activating newspaper Leni 的肖像很好将成为报纸争论的焦点
6 Leni.competition.Zeitung->TelefonNr 从报纸上的文章中获得电话号码
7 Leni.competition.deactivating TelNr 在通话和重新整理之后,取消那些无用的电话号码
8 deactivating Hale 由于 Leni 冒犯了 他,Hale 自己退出了
9 Hale recommends Josi 在闲聊中 Hale 向 Josi 介绍这个卖点(闲聊是很重要的!)
10 deactivating Josi Josi 自己退出
11 deactivating Peter Peter 自己退出
12 Sepp Nachricht without Leni aktivieren Sepp 定购非法产品,Leni 拒绝了,整个事情公开了。
13 Sepp Nachricht with Leni aktivieren Leni 接受了非法的产品,问题接踵而来
14 lost game 邮差 Gottfried 终止这个游戏
15 won game Gottfried 实现了热狗卖点的价值。同时 Leni 胜利了
16 Hale.news article Asyl activate Leni 同 Hale 由报纸上关于庇护权的文章谈到了他的家庭
17 Hale.news article->Telefonnr activate 报纸上出现的现在可用的电话号码
18 Hale->Zeitungsbericht->Telefonnr deactivating 会话使电话号码失效
19 Hale->Familie activating Hale 的家里受到了庇护
20 activating the spy Leni 应该雇用个侦探,但是这个功能还没有被付诸实现
33 New products 1 (New supplier) 扩大产品的范围
100 won contest Leni 赢了辩论赛,顾客开始讨论这件事...
101 losts contest
102 Lotteryprice Leni 中了彩票。
我们可以看到,事件的确是一个实现游戏逻辑性的强有力的工具。 

数学计算

用户可以通过命令 Rechne (Rechne 在德语中是计算的意思)来计算数学表达式并将它们保存在寄存器中。例如命令
Rechne [100]: 20 + [30] * 10
将会把寄存器 30 中的内容乘以 10 再加上 20,其结果保存在寄存器 100 中。

下面是可以使用的数学运算符:
运算符 符号 示例 结果
Klammern (a) (10+20)*30 900
Register [a] [20] 寄存器 20 中的内容
Multiplication a*b 3*4 12
Division a/b 10/5 2
Rest a%b 10%3 1
Addition a+b 1+1 2
Subtraction a-b 1-1 0
Zuweisung [a]:b [10]:20 将 20 写入寄存器 10
Vergleiche a?b 真(1)或者假(0)
Ist gleich a=b 10=20 假(0)
Kleiner a<b 10<20 真(1)
Größer a>b [10]>[20]
AND a&b 1=1 & 2=2 如果 1 等于 1 并且 2 等于 2
OR a|b 1=1 | 2=2 如果 1 等于 1 或者 2 等于 2
Random number a Z b 1 Z 6 返回一个 1 和 6 之间的随机数

比较的结果用数字来表示:1 代表“真”,0 代表“假/错”。这些数字还可以写入寄存器中。表达式中空格符可有可无。

开发数学求值程序是最具挑战性的。现在的数学求值程序已经能够处理象下面这样的表达式了:

Assumption: [100]=5, [24]=14, 1Z6=2

[[100]+1]:((1Z6)*([24]>3)+10/2-10%5)
[5    +1]:((2  )*(14  >3)+10/2-10%5)
[6      ]:(2    *(1        )+5   -0   )
[6      ]:(2    *1          +5        )
[6      ]:(7                          )
[6      ]:7
结果:[6]:7 数值 7 被写入寄存器 6 中。

 

会话对寄存器的操作

通过命令 Wenn (Wenn 在德语中是如果的意思):
Wenn condition
then
玩家可以实现比较操作。例如:
Wenn [100+1]>10
Kunde: The number in the register 101 is bigger than 10 !
Wenn 1>1
Kunde: ERROR!

如果条件为真解释程序将顺序执行下一行,否则跳过该行。该命令可以和跳转命令联合使用:

Wenn [102]<10
Sprung SMALLER
Wenn [102]=10
Sprung EQUAL
Wenn [102]>10
Sprung BIGGER
...
:SMALLER
...
:EQUAL
...
:BIGGER
 

显示图片

BILD expression
(Bild 在德语中是图片的意思)。例如,
Bild 5
是 HALE.BAT 中的一个命令行。当解释程序执行到此时,将会显示图片 HALE5.DAT(一种特殊的图片格式)。单击鼠标后会话继续。 

命令索引

为了是大家能够更方便的从整体上掌握 Dialog 的命令,我写如下这个命令索引:
// comment: 命令索引

Kunde: text 顾客发言
Tel: text 会话伙伴发言
Leni: text Leni 发言
:target 跳转目标
Liste number 指定当前列表
Löschen * 删除当前列表中的全部菜单项
Löschen target 删除当前列表中指向目标 target 的所有菜单项
Aktion number 激活事件
Ende 结束会话
Bild number 显示文件名为 NameNumber.dat 的图片
Sprung target 跳转到目标
Neu target,Text 在当前列表中插入新话题
Alt target,Text 在当前列表中插入新选项
Menü 显示菜单共用户选择
Wenn condition 比较判断(见下面两行)
//then 如果为真,解释程序执行下一行
//else 如果为假,解释程序跳过下一行
Rechne expression 计算表达式的值并存入寄存器中
Bild expression 显示图片并等待点击鼠标
 

多重选择系统的缺点

 

会话生成器

Markus Muntaneau 开发了一个叫做 Dialog-Maker 的程序。它使开发会话的过程见的简便易行了。遗憾的是他还没有做完(还存在这一些 Bug)。因此不是很有用。不过,作者建议 Dialog 的开发者们还是应该看看这个软件。

 

编程诀窍

整个 Würstelstand 项目含有 10,000 行 C(++) 代码,编译时间也可以接受。因此我没有把它写成比较清晰的模块方式(好吧,我承认是我太懒惰了)。这里我开发了一个 Test-Include 系统:模块的代码被集成到 a.c 文件中。此文件可以独立运行,同时还提供了一个根据这些模块编写的测试程序。这里用 #ifdef、#ifndef 等管理模块(头文件)的引用。

batch.cpp
#ifndef _DIALOG_H 
#define _DIALOG_H 
 
#ifndef MAIN_MODULE
  #define DIALOG_TEXT 
  #define DEBUG 
  //Here are the necessary included Header files
  #include <stdio.h>
  //... 
#endif 
 
//Here are the whole dialog routines
//..
S2 Dialog(char *Filename, TYP Array[])
{
  //...
}

#ifndef MAIN_MODULE
 //Here is everything for the test programs
TYP Feld[256]; 
int main(short argc,char *argv[]) 
{ 
  //Testprogram
  Dialog(Filename,Feld);
} 
#endif
wurst.cpp
#define MAIN_MODULE
#include "batch.cpp"
TYP Felder[10][256];
int main(short argc,char *argv[]) 
{ 
  Dialog(Filename,Felder[i]);
}

 

备注

仿真软件 Würstelstand 的 Linux 版本可以在 Futureware (http://poboxes.com/futureware)处得到。如果想下载 dialog 的 1.1 版本 请点击这里(dialog-1.1.tar.gz)。若有其它的需求请发信给作者。根据读者的需求,作者会考虑接着写些关于 Dialog 实际例子的文章。

中文翻译:郑新广

 


Webpages maintained by the LinuxFocus Editor team
© Philipp Gühring
LinuxFocus 1999

1999-09-04, generated by lfparser version 0.7