update
many, many chore
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# 13.编写解析器
|
||||
|
||||
每个文本冒险都有一个解析器,但是解析器也有高下之分。一个简单的 "动词-名词 "解析器(就像我们从第二章开始一直使用的那个)对于一个精心设计的冒险游戏来说可能已经足够了。
|
||||
每个文本冒险都有一个解析器,但是解析器也有高下之分。一个简单的 "动词 - 名词 "解析器(就像我们从第二章开始一直使用的那个)对于一个精心设计的冒险游戏来说可能已经足够了。
|
||||
|
||||
然而,Infocom 已经证明,一个更高级的解析器确实有助于制作一个令人愉快的游戏。它不一定要通过图灵测试。
|
||||
|
||||
@@ -19,7 +19,8 @@ char *noun = strtok(NULL, "\n");
|
||||
- 它确实接受多字对象(如银币),但字与字之间的空格必须准确无误。我们的游戏拒绝银币和硬币之间的双空格。
|
||||
- 它是区分大小写的;"向北走 "的命令因为大写的 "G "而不被识别。
|
||||
|
||||
思考题:你能想到有什么办法解决这些问题吗?
|
||||
::: warning 🤔 思考题:你能想到有什么办法解决这些问题吗?
|
||||
:::
|
||||
|
||||
编写一个好的分析器并不是一件小事,但在这里我将给你一个相对简单的方法,我们将定义一个由模式列表组成的语法,类似于(但比)正则表达式要简单得多。
|
||||
|
||||
@@ -30,7 +31,8 @@ char *noun = strtok(NULL, "\n");
|
||||
|
||||
为了解析用户的输入,我们将从上到下遍历模式列表,依次尝试将用户的输入与每个模式匹配。我们将在发现第一个匹配时停止。为了简单起见,我们将不使用回溯,尽管这可以在以后添加。
|
||||
|
||||
思考题:如果我们使用回溯,那该怎么编写代码?
|
||||
::: warning 🤔 思考题:如果我们使用回溯,那该怎么编写代码?
|
||||
:::
|
||||
|
||||
大写字母是我们语法中的非终端符号,它们可以匹配任何标签(任何对象)。当解析器在两个不同的标签(例如 "银币 "和 "银")之间进行选择时,较长的标签将被优先考虑。
|
||||
|
||||
@@ -40,7 +42,7 @@ char *noun = strtok(NULL, "\n");
|
||||
const char *params[26];
|
||||
```
|
||||
|
||||
该数组有 26 个元素;字母表中的每个(大写)字母都有一个,这足以满足一个模式中多达 26 个不同的非终端。对于一个(匹配的)模式中的每个非终端,将通过在非终端的数组元素中填充一个指向该特定标签的指针来 "捕获 "一个匹配的标签。 params[0](第一个数组元素)捕获非终端 "A",params[1]捕获 "B",以此类推。一个简单的宏定义可以用来找到属于某个非终端的数组元素。
|
||||
该数组有 26 个元素;字母表中的每个(大写)字母都有一个,这足以满足一个模式中多达 26 个不同的非终端。对于一个(匹配的)模式中的每个非终端,将通过在非终端的数组元素中填充一个指向该特定标签的指针来 "捕获 "一个匹配的标签。params[0](第一个数组元素)捕获非终端 "A",params[1]捕获 "B",以此类推。一个简单的宏定义可以用来找到属于某个非终端的数组元素。
|
||||
|
||||
```c
|
||||
#define paramByLetter(letter) (params + (letter) - 'A')
|
||||
@@ -52,7 +54,8 @@ const char *params[26];
|
||||
|
||||
假设用户打错了字,输入了 "go kave"。问题是,这个命令到底应不应该匹配 "go A "这个命令?如果我们不想出解决办法,那么这个命令将无法匹配任何其他命令,并最终进入一个通用的错误处理程序,它可能会回答 "我不知道如何去 kave "这样的话。这就失去了改进这些 "消极反应 "的所有机会;回答 "我不知道你要去哪里 "已经感觉更自然了。最好的办法是将所有关于命令去向的答复保持在函数 executeGo 内。
|
||||
|
||||
停下来想一想,可以怎么解决这个问题?
|
||||
::: warning 🤔 停下来想一想,可以怎么解决这个问题?
|
||||
:::
|
||||
|
||||
有几种方法可以实现这一点,但最简单的方法是允许非终止符匹配任何东西。所以不仅仅是一个有效的标签,也包括完全的胡言乱语、空白或什么都没有这种语句。这种 "无效 "的输入将被捕获为一个空字符串("")。
|
||||
|
||||
@@ -73,17 +76,18 @@ const char *params[26];
|
||||
|
||||
前两个命令形式可以为任何顺序,但第三个必须在最后。
|
||||
|
||||
思考题:你是否有办法解决这个问题?
|
||||
::: warning 🤔 思考题:你是否有办法解决这个问题?
|
||||
:::
|
||||
|
||||
是时候将其付诸行动了。我们将抛弃模块 parsexec.c 的现有内容,用一个新的函数 parseAndExecute 的实现来取代它,该函数使用一个模式列表,应该能够匹配我们到目前为止实现的每一条命令。每个模式都与一个执行相应命令的函数相联系。
|
||||
|
||||
# parsexec.c
|
||||
## parsexec.c
|
||||
|
||||
```c
|
||||
extern bool parseAndExecute(const char *input);
|
||||
```
|
||||
|
||||
# parsexec.h
|
||||
## parsexec.h
|
||||
|
||||
```c
|
||||
#include <ctype.h>
|
||||
@@ -149,7 +153,7 @@ bool parseAndExecute(const char *input)
|
||||
|
||||
最难的部分是函数 matchCommand 的实现。但正如你在下面看到的,这也可以在不到 100 行的代码中完成。
|
||||
|
||||
# match.h
|
||||
## match.h
|
||||
|
||||
```c
|
||||
#define MAX_PARAMS 26
|
||||
@@ -161,7 +165,7 @@ extern const char *params[];
|
||||
extern bool matchCommand(const char *src, const char *pattern);
|
||||
```
|
||||
|
||||
# match.c
|
||||
## match.c
|
||||
|
||||
```c
|
||||
#include <ctype.h>
|
||||
@@ -242,7 +246,7 @@ bool matchCommand(const char *src, const char *pattern)
|
||||
|
||||
我们调整各种命令的实现,以利用新的数组参数。
|
||||
|
||||
# <strong>inventory.h</strong>
|
||||
## <strong>inventory.h</strong>
|
||||
|
||||
```c
|
||||
extern bool executeGet(void);
|
||||
@@ -252,7 +256,7 @@ extern bool executeGive(void);
|
||||
extern bool executeInventory(void);
|
||||
```
|
||||
|
||||
# <strong>inventory.c</strong>
|
||||
## <strong>inventory.c</strong>
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
@@ -323,7 +327,7 @@ bool executeInventory(void)
|
||||
|
||||
我们在上一章中添加的模块也是如此。
|
||||
|
||||
# opemclose.h
|
||||
## opemclose.h
|
||||
|
||||
```c
|
||||
extern bool executeOpen(void);
|
||||
@@ -332,7 +336,7 @@ extern bool executeLock(void);
|
||||
extern bool executeUnlock(void);
|
||||
```
|
||||
|
||||
# openclose.c
|
||||
## openclose.c
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
@@ -372,7 +376,7 @@ bool executeUnlock(void)
|
||||
|
||||
在 location.c 中,look around 命令被赋予了自己的功能,与检查特定对象的 look 命令分开(见第 7-12 行)。
|
||||
|
||||
# location.h
|
||||
## location.h
|
||||
|
||||
```c
|
||||
extern bool executeLookAround(void);
|
||||
@@ -380,7 +384,7 @@ extern bool executeLook(void);
|
||||
extern bool executeGo(void);
|
||||
```
|
||||
|
||||
# location.c
|
||||
## location.c
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
@@ -453,7 +457,7 @@ bool executeGo(void)
|
||||
}
|
||||
```
|
||||
|
||||
我们的游戏仍然只接受简单的动词-名词命令,但新的解析器确实有可能接受有多个名词的命令,如 "put coin in box",这将在下一章中演示。
|
||||
我们的游戏仍然只接受简单的动词 - 名词命令,但新的解析器确实有可能接受有多个名词的命令,如 "put coin in box",这将在下一章中演示。
|
||||
|
||||
新的解析器比原来的解析器有了很大的改进,但以今天的标准来看,它仍然远远不够完美。例如,没有结构性的方法可以用一个命令操纵多个对象("获得硬币、钥匙和灯")或连续执行两个或多个命令("获得钥匙然后向东走")。这对于一个 RPG 游戏来说限制实在太大了!
|
||||
|
||||
|
||||
Reference in New Issue
Block a user