12. 开启关闭
在上一章中,我们使用 "条件" 函数来使对象消失。当然,还有一个更简单的方法来实现同样的目的:只要清除对象的位置属性就可以了!
洞口是一个典型的例子,条件函数在那里工作得特别好。这是因为入口受到其他对象(守卫和银币)中的属性的影响;我们可以使用函数使得所有的逻辑都能保持一致。
让我们举一个更直接的例子。假设山洞有一扇门通向一个密室。只是一个简单的门洞,玩家可以打开和关闭。就像前一章一样,我们将使用两个对象来表示这个通道;一个表示打开的门,另一个表示门关闭时。
- backroom
description "a backroom"
tags "backroom"
details "The room is dusty and messy.\n"
- openDoorToBackroom
description "an open door to the south"
tags "south", "door", "doorway"
destination backroom
details "The door is open.\n"
textGo "You walk through the door into the backroom.\n"
- closedDoorToBackroom
description "a closed door to the south"
tags "south", "door", "doorway"
location cave
prospect backroom
details "The door is closed.\n"
textGo "The door is closed.\n"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
🤔 思考题:尝试自己用 C 语言实现
自然,门也应该能从另一侧进入。
- openDoorToCave
description "an open door to the north"
tags "north", "door", "doorway"
destination cave
details "The door is open.\n"
textGo "You walk through the door into the cave.\n"
- closedDoorToCave
description "a closed door to the north"
tags "north", "door", "doorway"
location backroom
prospect cave
details "The door is closed.\n"
textGo "The door is closed.\n"
2
3
4
5
6
7
8
9
10
11
12
13
14
注意我们只给封闭的门洞一个位置,开放的门洞没有。所以一开始,门是关闭的(因此你在右边看到的生成的地图中,山洞和密室之间有虚线箭头)。要打开门,我们所要做的就是交换位置。
openDoorToBackroom->location = cave;
closedDoorToBackroom->location = NULL;
openDoorToCave->location = backroom;
closedDoorToCave->location = NULL;
2
3
4
接下来,让我们创建一个辅助函数来适应这种情况。
void swapLocations(OBJECT *obj1, OBJECT *obj2)
{
OBJECT *tmp = obj1->location;
obj1->location = obj2->location;
obj2->location = tmp;
}
2
3
4
5
6
现在可以用下面的语句来打开这扇门;一旦它被打开,同样的语句将再次关闭它。
swapLocations(openDoorToBackroom, closedDoorToBackroom);
swapLocations(openDoorToCave, closedDoorToCave);
2
函数 swapLocations 不依赖于固定的位置,因为它在两个对象之间来回传递当前位置。
当相关对象是可移动的时候,辅助功能就特别方便。例如,一个盒子可以被打开和关闭,但它也是一个可以被拿起并移动到其他地方的物品。换句话说,它的位置是不固定的。
当然,一个盒子不是一个通道:玩家总是在盒子外面。所以一对物体就够了,也就是说,我们调用一次 swapLocations 函数就够了。
swapLocations(openBox, closedBox);
这或多或少是我们实现一些新命令 open 和 close 所需要的。下面是 open 的一个简单实现;close 的实现也类似。
OBJECT *obj = parseObject(noun);
if (obj == closedDoorToBackRoom || obj == closedDoorToCave)
{
swapLocations(openDoorToBackroom, closedDoorToBackroom);
swapLocations(openDoorToCave, closedDoorToCave);
printf("OK.\n");
}
else if (obj == closedBox)
{
swapLocations(openBox, closedBox);
printf("OK.\n");
}
else if (obj == openDoorToBackRoom || obj == openDoorToCave || obj == openBox)
{
printf("That is already open.\n");
}
else
{
printf("That cannot be opened.\n");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
🤔 思考题:你能不能仿照上面的代码实现 close 功能?
为了使事情稍微复杂一些,我们可以在门上或盒子上加一把锁。这需要(至少)三个相互排斥的对象;每个可能的状态都有一个:打开、关闭和锁定。但是我们仍然可以使用同一个函数来交换对象的位置。例如,这里是如何解锁一个上锁的盒子;反之亦然。
swapLocations(closedBox, lockedBox);
但这仅仅是不够的,我们对命令 open 的实现必须进行扩展,以处理新的对象 lockedBox。
...
else if (obj == lockedBox)
{
printf("You can't, it is locked.\n");
}
...
2
3
4
5
6
显然,代码的行数与游戏中的门(以及盒子和其他可以打开的物体)的数量成正比。因此,如果你的游戏有不止几扇门,那么选择一个更通用的解决方案是个好主意。顺便说一下,这对每一个命令都是适用的:当它涉及到许多物体时,尽量写通用代码;但当你处理一两个特殊情况时,就坚持使用直接的、专门的代码。
🤔 思考题:
我们可以使用什么方法来解决这个问题?
提示:C++ 中的模板功能(这只是一种选择)
下面我们将揭晓答案
通用代码通常带有数据驱动的方法。换句话说,我们需要向我们的对象结构添加一个或多个属性。在这种特殊情况下,我们将为我们希望支持的每个命令添加一个函数指针:打开、关闭、锁定和解锁。
object.txt
#include <stdbool.h>
#include <stdio.h>
#include "object.h"
#include "toggle.h"
typedef struct object {
bool (*condition)(void);
const char *description;
const char **tags;
struct object *location;
struct object *destination;
struct object *prospect;
const char *details;
const char *contents;
const char *textGo;
int weight;
int capacity;
int health;
void (*open)(void);
void (*close)(void);
void (*lock)(void);
void (*unlock)(void);
} OBJECT;
extern OBJECT objs[];
- field
description "an open field"
tags "field"
details "The field is a nice and quiet place under a clear blue sky."
capacity 9999
- cave
description "a little cave"
tags "cave"
details "The cave is just a cold, damp, rocky chamber."
capacity 9999
- silver
description "a silver coin"
tags "silver", "coin", "silver coin"
location field
details "The coin has an eagle on the obverse."
weight 1
- gold
description "a gold coin"
tags "gold", "coin", "gold coin"
location openBox
details "The shiny coin seems to be a rare and priceless artefact."
weight 1
- guard
description "a burly guard"
tags "guard", "burly guard"
location field
details "The guard is a really big fellow."
contents "He has"
health 100
capacity 20
- player
description "yourself"
tags "yourself"
location field
details "You would need a mirror to look at yourself."
contents "You have"
health 100
capacity 20
- intoCave
condition { return guard->health == 0 || silver->location == guard; }
description "a cave entrance to the east"
tags "east", "entrance"
location field
destination cave
details "The entrance is just a narrow opening in a small outcrop."
textGo "You walk into the cave."
open isAlreadyOpen
- intoCaveBlocked
condition { return guard->health > 0 && silver->location != guard; }
description "a cave entrance to the east"
tags "east", "entrance"
location field
prospect cave
details "The entrance is just a narrow opening in a small outcrop."
textGo "The guard stops you from walking into the cave."
open isAlreadyOpen
- exitCave
description "an exit to the west"
tags "west", "exit"
location cave
destination field
details "Sunlight pours in through an opening in the cave's wall."
textGo "You walk out of the cave."
open isAlreadyOpen
- wallField
description "dense forest all around"
tags "west", "north", "south", "forest"
location field
details "The field is surrounded by trees and undergrowth."
textGo "Dense forest is blocking the way."
- wallCave
description "solid rock all around"
tags "east", "north", "rock"
location cave
details "Carved in stone is a secret password 'abccb'."
textGo "Solid rock is blocking the way."
- backroom
description "a backroom"
tags "backroom"
details "The room is dusty and messy."
capacity 9999
- wallBackroom
description "solid rock all around"
tags "east", "west", "south", "rock"
location backroom
details "Trendy wallpaper covers the rock walls."
textGo "Solid rock is blocking the way."
- openDoorToBackroom
description "an open door to the south"
tags "south", "door", "doorway"
destination backroom
details "The door is open."
textGo "You walk through the door into a backroom."
open isAlreadyOpen
close toggleDoorToBackroom
- closedDoorToBackroom
description "a closed door to the south"
tags "south", "door", "doorway"
location cave
prospect backroom
details "The door is closed."
textGo "The door is closed."
open toggleDoorToBackroom
close isAlreadyClosed
- openDoorToCave
description "an open door to the north"
tags "north", "door", "doorway"
destination cave
details "The door is open."
textGo "You walk through the door into the cave."
open isAlreadyOpen
close toggleDoorToCave
- closedDoorToCave
description "a closed door to the north"
tags "north", "door", "doorway"
location backroom
prospect cave
details "The door is closed."
textGo "The door is closed."
open toggleDoorToCave
close isAlreadyClosed
- openBox
description "a wooden box"
tags "box", "wooden box"
details "The box is open."
weight 5
capacity 10
open isAlreadyOpen
close toggleBox
lock isStillOpen
unlock isAlreadyOpen
- closedBox
description "a wooden box"
tags "box", "wooden box"
details "The box is closed."
weight 5
open toggleBox
close isAlreadyClosed
lock toggleBoxLock
unlock isAlreadyUnlocked
- lockedBox
description "a wooden box"
tags "box", "wooden box"
location backroom
details "The box is closed."
weight 5
open isStillLocked
close isAlreadyClosed
lock isAlreadyLocked
unlock toggleBoxLock
- keyForBox
description "a tiny key"
tags "key", "tiny key"
location cave
details "The key is really small and shiny."
weight 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
注意:
- 第 89 行:乍一看,isAlreadyOpen 在这里似乎不合适;从技术上讲,intoCaveBlocked 是一个封闭的通道。但从故事上讲,它是一个不错的开场白。
- 第 169、180、191 行。如果你喜欢沉重的宝箱而不是盒子,那么你所要做的就是增加重量(并相应调整相关文字和标签)。
为了避免重复的代码,我们这次特意没有使用匿名函数。相反,我们将在一个单独的模块中实现必要的逻辑。函数 swapLocations 也在其中,这不过是一个稍微扩展的版本,它也会向用户输出反馈。
toggle.h
extern void cannotBeOpened(void);
extern void cannotBeClosed(void);
extern void cannotBeLocked(void);
extern void cannotBeUnlocked(void);
extern void isAlreadyOpen(void);
extern void isAlreadyClosed(void);
extern void isAlreadyLocked(void);
extern void isAlreadyUnlocked(void);
extern void isStillOpen(void);
extern void isStillLocked(void);
extern void toggleDoorToBackroom(void);
extern void toggleDoorToCave(void);
extern void toggleBox(void);
extern void toggleBoxLock(void);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
toggle.c
#include <stdbool.h>
#include <stdio.h>
#include "object.h"
static void swapLocations(const char *verb1, OBJECT *obj1,
const char *verb2, OBJECT *obj2)
{
OBJECT *tmp = obj1->location;
OBJECT *obj = tmp != NULL ? obj1 : obj2;
const char *verb = tmp != NULL ? verb1 : verb2;
obj1->location = obj2->location;
obj2->location = tmp;
if (verb != NULL) printf("You %s %s.\n", verb, obj->description);
}
void cannotBeOpened(void) { printf("That cannot be opened.\n"); }
void cannotBeClosed(void) { printf("That cannot be closed.\n"); }
void cannotBeLocked(void) { printf("That cannot be locked.\n"); }
void cannotBeUnlocked(void) { printf("That cannot be unlocked.\n"); }
void isAlreadyOpen(void) { printf("That is already open.\n"); }
void isAlreadyClosed(void) { printf("That is already closed.\n"); }
void isAlreadyLocked(void) { printf("That is already locked.\n"); }
void isAlreadyUnlocked(void) { printf("That is already unlocked.\n"); }
void isStillOpen(void) { printf("That is still open.\n"); }
void isStillLocked(void) { printf("That is locked.\n"); }
void toggleDoorToBackroom(void)
{
swapLocations(NULL, openDoorToCave, NULL, closedDoorToCave);
swapLocations("close", openDoorToBackroom, "open", closedDoorToBackroom);
}
void toggleDoorToCave(void)
{
swapLocations(NULL, openDoorToBackroom, NULL, closedDoorToBackroom);
swapLocations("close", openDoorToCave, "open", closedDoorToCave);
}
void toggleBox(void)
{
swapLocations("close", openBox, "open", closedBox);
}
void toggleBoxLock(void)
{
if (keyForBox->location == player)
{
swapLocations("lock", closedBox, "unlock", lockedBox);
}
else
{
printf("You don't have a key.\n");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
正如前面所宣布的,打开、关闭、锁定和解锁这四个命令的实现是完全通用的。
openclose.h
extern void executeOpen(const char *noun);
extern void executeClose(const char *noun);
extern void executeLock(const char *noun);
extern void executeUnlock(const char *noun);
2
3
4
openclose.c
#include <stdbool.h>
#include <stdio.h>
#include "object.h"
#include "reach.h"
void executeOpen(const char *noun)
{
OBJECT *obj = reachableObject("what you want to open", noun);
if (obj != NULL) (*obj->open)();
}
void executeClose(const char *noun)
{
OBJECT *obj = reachableObject("what you want to close", noun);
if (obj != NULL) (*obj->close)();
}
void executeLock(const char *noun)
{
OBJECT *obj = reachableObject("what you want to lock", noun);
if (obj != NULL) (*obj->lock)();
}
void executeUnlock(const char *noun)
{
OBJECT *obj = reachableObject("what you want to unlock", noun);
if (obj != NULL) (*obj->unlock)();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
上面,我们使用了一个通用函数 reachableObject 来处理不在这里的对象;其实现见下文。这样,我们就不必把同样的代码写四遍(每个执行函数写一遍)。更多的命令将在第 15 章中加入;这些命令将受益于同样的函数。
reach.h
extern OBJECT *reachableObject(const char *intention, const char *noun);
reach.c
#include <stdbool.h>
#include <stdio.h>
#include "object.h"
#include "misc.h"
#include "noun.h"
OBJECT *reachableObject(const char *intention, const char *noun)
{
OBJECT *obj = getVisible(intention, noun);
switch (getDistance(player, obj))
{
case distSelf:
printf("You should not be doing that to yourself.\n");
break;
case distHeldContained:
case distHereContained:
printf("You would have to get it from %s first.\n",
obj->location->description);
break;
case distOverthere:
printf("Too far away, move closer please.\n");
break;
case distNotHere:
case distUnknownObject:
// already handled by getVisible
break;
default:
return obj;
}
return NULL;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
同样,我们也要对 parsexec.c 进行补充
parsexec.c
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include "location.h"
#include "inventory.h"
#include "openclose.h"
bool parseAndExecute(char *input)
{
char *verb = strtok(input, " \n");
char *noun = strtok(NULL, "\n");
if (verb != NULL)
{
if (strcmp(verb, "quit") == 0)
{
return false;
}
else if (strcmp(verb, "look") == 0)
{
executeLook(noun);
}
else if (strcmp(verb, "go") == 0)
{
executeGo(noun);
}
else if (strcmp(verb, "get") == 0)
{
executeGet(noun);
}
else if (strcmp(verb, "drop") == 0)
{
executeDrop(noun);
}
else if (strcmp(verb, "give") == 0)
{
executeGive(noun);
}
else if (strcmp(verb, "ask") == 0)
{
executeAsk(noun);
}
else if (strcmp(verb, "inventory") == 0)
{
executeInventory();
}
else if (strcmp(verb, "open") == 0)
{
executeOpen(noun);
}
else if (strcmp(verb, "close") == 0)
{
executeClose(noun);
}
else if (strcmp(verb, "lock") == 0)
{
executeLock(noun);
}
else if (strcmp(verb, "unlock") == 0)
{
executeUnlock(noun);
}
else
{
printf("I don't know how to '%s'.\n", verb);
}
}
return true;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
注意:
- 你可能已经注意到,object.txt 在本章中的大小几乎增加了一倍。我们已经可以向你保证,这只是一个开始。object.txt 是我们游戏数据的主要来源;一旦我们认真开始添加地点、物品和演员,行数很容易就会增加到数千行。
- 除了门和锁之外,函数 swapLocation 还可以用于许多其他事情。在第 15 章,它将再次被使用,这次是用来打开和关闭灯。
- 正如你在样本输出中所看到的,玩家可以从盒子里拿到金子,但他无法再把金子放回去!我们的解析器无法处理像把硬币放进盒子这样的 "复杂" 命令。因此,在下一章中,我们将编写一个全新的解析器:目前的双线实现急需更换!
输出样例
Welcome to Little Cave Adventure. You are in an open field. You see: a silver coin a burly guard a cave entrance to the east dense forest all around
--> get coin You pick up a silver coin.
--> give coin You give a silver coin to a burly guard.
--> go cave You walk into the cave.
You are in a little cave. You see: an exit to the west solid rock all around a closed door to the south a tiny key
--> get key You pick up a tiny key.
--> go south The door is closed.
--> open door You open a closed door to the south.
--> go south You walk through the door into a backroom.
You are in a backroom. You see: solid rock all around an open door to the north a wooden box
--> unlock box You unlock a wooden box.
--> open box You open a wooden box.
--> look box The box is open. You see: a gold coin
--> get gold You get a gold coin from a wooden box.
--> quit
Bye!