kni 4.0.3 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +9 -7
- package/console.js +33 -35
- package/describe.js +54 -89
- package/document.js +103 -72
- package/engine.js +436 -407
- package/entry.js +88 -0
- package/evaluate.js +221 -228
- package/excerpt.js +117 -115
- package/grammar.js +1025 -785
- package/html.js +174 -167
- package/inline-lexer.js +155 -125
- package/kni.js +286 -279
- package/link.js +50 -52
- package/outline-lexer.js +64 -37
- package/package.json +27 -34
- package/parser.js +32 -20
- package/path.js +34 -46
- package/readline.js +89 -79
- package/scanner.js +101 -78
- package/scope.js +32 -36
- package/story.js +174 -165
- package/test.js +6 -0
- package/translate-json.js +3 -5
- package/tsconfig.json +11 -0
- package/verify.js +121 -117
- package/wrapper.js +37 -41
- package/template.js +0 -69
package/engine.js
CHANGED
|
@@ -1,246 +1,299 @@
|
|
|
1
|
-
|
|
1
|
+
import evaluate from './evaluate.js';
|
|
2
|
+
import describe from './describe.js';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
const weigh = (scope, randomer, expressions, weights) => {
|
|
5
|
+
let weight = 0;
|
|
6
|
+
for (let i = 0; i < expressions.length; i++) {
|
|
7
|
+
weights[i] = evaluate(scope, randomer, expressions[i]);
|
|
8
|
+
weight += weights[i];
|
|
9
|
+
}
|
|
10
|
+
return weight;
|
|
11
|
+
};
|
|
6
12
|
|
|
7
|
-
|
|
13
|
+
const pick = (weights, weight, randomer) => {
|
|
14
|
+
const offset = Math.floor(randomer.random() * weight);
|
|
15
|
+
let passed = 0;
|
|
16
|
+
for (let i = 0; i < weights.length; i++) {
|
|
17
|
+
passed += weights[i];
|
|
18
|
+
if (offset < passed) {
|
|
19
|
+
return i;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const pop = (array, index) => {
|
|
26
|
+
array[index] = array[array.length - 1];
|
|
27
|
+
array.length--;
|
|
28
|
+
};
|
|
8
29
|
|
|
9
|
-
|
|
30
|
+
export default class Engine {
|
|
31
|
+
debug = typeof process === 'object' && process.env.DEBUG_ENGINE;
|
|
10
32
|
|
|
11
|
-
|
|
12
|
-
// istanbul ignore next
|
|
13
|
-
var self = this;
|
|
33
|
+
constructor(args) {
|
|
14
34
|
this.story = args.story;
|
|
15
35
|
this.labels = Object.keys(this.story);
|
|
16
36
|
this.handler = args.handler;
|
|
37
|
+
this.meter = 0;
|
|
38
|
+
this.limit = 10e3; // bottles.kni, for example, runs long
|
|
17
39
|
this.options = [];
|
|
18
40
|
this.keywords = {};
|
|
19
41
|
this.noOption = null;
|
|
20
42
|
this.global = new Global(this.handler);
|
|
21
43
|
this.top = this.global;
|
|
22
|
-
// istanbul ignore next
|
|
23
44
|
this.start = args.start || 'start';
|
|
24
45
|
this.label = this.start;
|
|
25
|
-
this.instruction =
|
|
46
|
+
this.instruction = {type: 'goto', next: this.start};
|
|
26
47
|
this.render = args.render;
|
|
27
48
|
this.dialog = args.dialog;
|
|
28
49
|
this.dialog.engine = this;
|
|
29
|
-
// istanbul ignore next
|
|
30
50
|
this.randomer = args.randomer || Math;
|
|
31
|
-
this.debug = debug;
|
|
32
51
|
this.waypoint = this.capture();
|
|
52
|
+
this.answerOnClearMeterFault = [];
|
|
33
53
|
Object.seal(this);
|
|
34
|
-
}
|
|
54
|
+
}
|
|
35
55
|
|
|
36
|
-
|
|
37
|
-
|
|
56
|
+
reset() {
|
|
57
|
+
this.labels = Object.keys(this.story);
|
|
58
|
+
this.options = [];
|
|
59
|
+
this.keywords = {};
|
|
60
|
+
this.noOption = null;
|
|
61
|
+
this.global = new Global(this.handler);
|
|
62
|
+
this.top = this.global;
|
|
63
|
+
this.label = this.start;
|
|
64
|
+
this.instruction = {type: 'goto', next: this.start};
|
|
65
|
+
this.waypoint = this.capture();
|
|
38
66
|
this.resume();
|
|
39
|
-
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Runs the event loop until it yields.
|
|
71
|
+
*/
|
|
72
|
+
continue() {
|
|
73
|
+
this.meter = 0;
|
|
74
|
+
for (;;) {
|
|
75
|
+
if (this.debug) {
|
|
76
|
+
console.log(`${this.label} ${this.instruction.type} ${describe(this.instruction)}`);
|
|
77
|
+
console.log(this.top);
|
|
78
|
+
}
|
|
79
|
+
if (this.instruction == null) {
|
|
80
|
+
// TODO user error for non-console interaction.
|
|
81
|
+
console.log(`The label ${JSON.stringify(this.label)} does not exist in this story`);
|
|
82
|
+
this.end();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (!this[`$${this.instruction.type}`]) {
|
|
86
|
+
console.error(`Unexpected instruction type: ${this.instruction.type}`, this.instruction);
|
|
87
|
+
this.resume();
|
|
88
|
+
}
|
|
89
|
+
const proceed = this[`$${this.instruction.type}`](this.instruction);
|
|
90
|
+
if (!proceed) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
this.meter += 1;
|
|
94
|
+
if (this.meter >= this.limit) {
|
|
95
|
+
this.display();
|
|
96
|
+
this.dialog.meterFault();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
40
101
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (this.debug) {
|
|
46
|
-
console.log(this.label + ' ' + this.instruction.type + ' ' + describe(this.instruction));
|
|
47
|
-
}
|
|
48
|
-
// istanbul ignore if
|
|
49
|
-
if (this.instruction == null) {
|
|
50
|
-
// TODO user error for non-console interaction.
|
|
51
|
-
console.log('The label ' + JSON.stringify(this.label) + ' does not exist in this story');
|
|
52
|
-
this.end();
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
// istanbul ignore if
|
|
56
|
-
if (!this['$' + this.instruction.type]) {
|
|
57
|
-
console.error('Unexpected instruction type: ' + this.instruction.type, this.instruction);
|
|
58
|
-
this.resume();
|
|
59
|
-
}
|
|
60
|
-
_continue = this['$' + this.instruction.type](this.instruction);
|
|
61
|
-
} while (_continue);
|
|
62
|
-
};
|
|
102
|
+
clearMeterFault() {
|
|
103
|
+
if (this.meter >= this.limit) {
|
|
104
|
+
this.render.clear();
|
|
105
|
+
this.continue();
|
|
63
106
|
|
|
64
|
-
|
|
107
|
+
// flush answers posted while faulted
|
|
108
|
+
const count = this.answerOnClearMeterFault.length;
|
|
109
|
+
const answers = this.answerOnClearMeterFault.splice(0, count);
|
|
110
|
+
for (const answer of answers) {
|
|
111
|
+
this.answer(answer);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
goto(label) {
|
|
65
117
|
while (this.top != null && (label == 'ESC' || label === 'RET')) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
this.top = this.top.parent;
|
|
118
|
+
if (this.debug) {
|
|
119
|
+
console.log(label.toLowerCase());
|
|
120
|
+
}
|
|
121
|
+
if (this.top.stopOption) {
|
|
122
|
+
this.render.stopOption();
|
|
123
|
+
}
|
|
124
|
+
if (label === 'ESC') {
|
|
125
|
+
label = this.top.branch;
|
|
126
|
+
} else {
|
|
127
|
+
label = this.top.next;
|
|
128
|
+
}
|
|
129
|
+
this.top = this.top.parent;
|
|
79
130
|
}
|
|
80
131
|
|
|
81
132
|
if (label === 'RET') {
|
|
82
|
-
|
|
133
|
+
return this.end();
|
|
83
134
|
}
|
|
84
135
|
|
|
85
|
-
|
|
86
|
-
// istanbul ignore if
|
|
136
|
+
const next = this.story[label];
|
|
87
137
|
if (!next) {
|
|
88
|
-
|
|
89
|
-
|
|
138
|
+
console.error('Story missing label', label);
|
|
139
|
+
return this.resume();
|
|
90
140
|
}
|
|
91
|
-
// istanbul ignore if
|
|
92
141
|
if (!next) {
|
|
93
|
-
|
|
94
|
-
|
|
142
|
+
console.error(`Story missing instruction for label: ${label}`);
|
|
143
|
+
return this.resume();
|
|
95
144
|
}
|
|
96
145
|
if (this.handler && this.handler.goto) {
|
|
97
|
-
|
|
146
|
+
this.handler.goto(label, next);
|
|
98
147
|
}
|
|
99
148
|
this.label = label;
|
|
100
149
|
this.instruction = next;
|
|
101
150
|
return true;
|
|
102
|
-
}
|
|
151
|
+
}
|
|
103
152
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
for (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
153
|
+
gothrough(sequence, next) {
|
|
154
|
+
let prev = this.label;
|
|
155
|
+
for (let i = sequence.length - 1; i >= 0; i--) {
|
|
156
|
+
if (next !== 'RET') {
|
|
157
|
+
this.top = new Frame(this.top, [], next, 'RET', prev);
|
|
158
|
+
}
|
|
159
|
+
prev = next;
|
|
160
|
+
next = sequence[i];
|
|
112
161
|
}
|
|
113
162
|
return this.goto(next);
|
|
114
|
-
}
|
|
163
|
+
}
|
|
115
164
|
|
|
116
|
-
|
|
165
|
+
end() {
|
|
166
|
+
this.display();
|
|
117
167
|
if (this.handler && this.handler.end) {
|
|
118
|
-
|
|
168
|
+
this.handler.end(this);
|
|
119
169
|
}
|
|
120
|
-
this.display();
|
|
121
170
|
this.dialog.close();
|
|
122
171
|
return false;
|
|
123
|
-
}
|
|
172
|
+
}
|
|
124
173
|
|
|
125
|
-
|
|
174
|
+
ask() {
|
|
126
175
|
if (this.options.length) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
176
|
+
this.display();
|
|
177
|
+
if (this.handler && this.handler.ask) {
|
|
178
|
+
this.handler.ask(this);
|
|
179
|
+
}
|
|
180
|
+
this.dialog.ask();
|
|
132
181
|
} else if (this.noOption != null) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
182
|
+
const closure = this.noOption;
|
|
183
|
+
const option = this.story[closure.label];
|
|
184
|
+
this.top = closure.scope;
|
|
185
|
+
const answer = option.answer;
|
|
186
|
+
this.flush();
|
|
187
|
+
this.gothrough(answer, 'RET');
|
|
188
|
+
this.continue();
|
|
140
189
|
} else {
|
|
141
|
-
|
|
190
|
+
return this.goto('RET');
|
|
142
191
|
}
|
|
143
|
-
}
|
|
192
|
+
}
|
|
144
193
|
|
|
145
|
-
|
|
194
|
+
read() {
|
|
146
195
|
this.display();
|
|
147
196
|
if (this.handler && this.handler.ask) {
|
|
148
|
-
|
|
197
|
+
this.handler.ask(this);
|
|
149
198
|
}
|
|
150
199
|
this.dialog.ask(this.instruction.cue);
|
|
151
|
-
}
|
|
200
|
+
}
|
|
152
201
|
|
|
153
|
-
|
|
202
|
+
answer(text) {
|
|
203
|
+
if (this.meter >= this.limit) {
|
|
204
|
+
this.answerOnClearMeterFault.push(text);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
154
207
|
if (this.handler && this.handler.answer) {
|
|
155
|
-
|
|
208
|
+
this.handler.answer(text, this);
|
|
156
209
|
}
|
|
157
210
|
this.render.flush();
|
|
158
211
|
if (this.instruction.type === 'read') {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
212
|
+
this.top.set(this.instruction.variable, text);
|
|
213
|
+
this.render.clear();
|
|
214
|
+
if (this.goto(this.instruction.next)) {
|
|
215
|
+
this.continue();
|
|
216
|
+
}
|
|
217
|
+
return;
|
|
165
218
|
}
|
|
166
|
-
|
|
219
|
+
const choice = text - 1;
|
|
167
220
|
if (choice >= 0 && choice < this.options.length) {
|
|
168
|
-
|
|
221
|
+
return this.choice(this.options[choice]);
|
|
169
222
|
} else if (this.keywords[text]) {
|
|
170
|
-
|
|
223
|
+
return this.choice(this.keywords[text]);
|
|
171
224
|
} else {
|
|
172
|
-
|
|
173
|
-
|
|
225
|
+
this.render.pardon();
|
|
226
|
+
this.ask();
|
|
174
227
|
}
|
|
175
|
-
}
|
|
228
|
+
}
|
|
176
229
|
|
|
177
|
-
|
|
178
|
-
|
|
230
|
+
choice(closure) {
|
|
231
|
+
const option = this.story[closure.label];
|
|
179
232
|
if (this.handler && this.handler.choice) {
|
|
180
|
-
|
|
233
|
+
this.handler.choice(option, this);
|
|
181
234
|
}
|
|
182
235
|
this.render.clear();
|
|
183
236
|
this.waypoint = this.capture(closure);
|
|
184
237
|
if (this.handler && this.handler.waypoint) {
|
|
185
|
-
|
|
238
|
+
this.handler.waypoint(this.waypoint, this);
|
|
186
239
|
}
|
|
187
240
|
// Resume in the option's closure scope.
|
|
188
241
|
this.top = closure.scope;
|
|
189
242
|
// There is no known case where gothrough would immediately exit for
|
|
190
243
|
// lack of further instructions, so
|
|
191
|
-
// istanbul ignore else
|
|
192
244
|
if (this.gothrough(option.answer, 'RET')) {
|
|
193
|
-
|
|
194
|
-
|
|
245
|
+
this.flush();
|
|
246
|
+
this.continue();
|
|
195
247
|
}
|
|
196
|
-
}
|
|
248
|
+
}
|
|
197
249
|
|
|
198
|
-
|
|
250
|
+
display() {
|
|
199
251
|
this.render.display();
|
|
200
|
-
}
|
|
252
|
+
}
|
|
201
253
|
|
|
202
|
-
|
|
254
|
+
flush() {
|
|
203
255
|
this.options.length = 0;
|
|
204
256
|
this.noOption = null;
|
|
205
257
|
this.keywords = {};
|
|
206
|
-
}
|
|
258
|
+
}
|
|
207
259
|
|
|
208
|
-
|
|
260
|
+
write(text) {
|
|
209
261
|
this.render.write(this.instruction.lift, text, this.instruction.drop);
|
|
210
262
|
return this.goto(this.instruction.next);
|
|
211
|
-
}
|
|
263
|
+
}
|
|
212
264
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
var label, top;
|
|
265
|
+
capture(closure) {
|
|
266
|
+
let label, top;
|
|
216
267
|
if (closure != null) {
|
|
217
|
-
|
|
218
|
-
|
|
268
|
+
label = closure.label;
|
|
269
|
+
top = closure.scope;
|
|
219
270
|
} else {
|
|
220
|
-
|
|
221
|
-
|
|
271
|
+
label = this.label;
|
|
272
|
+
top = this.top;
|
|
222
273
|
}
|
|
223
274
|
|
|
224
|
-
|
|
275
|
+
const stack = [];
|
|
225
276
|
for (; top != this.global; top = top.parent) {
|
|
226
|
-
|
|
277
|
+
stack.push(top.capture(this));
|
|
227
278
|
}
|
|
228
279
|
|
|
229
280
|
return [
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
281
|
+
this.indexOfLabel(label),
|
|
282
|
+
stack,
|
|
283
|
+
this.global.capture(),
|
|
284
|
+
[
|
|
285
|
+
this.randomer._state0U | 0,
|
|
286
|
+
this.randomer._state0L | 0,
|
|
287
|
+
this.randomer._state1U | 0,
|
|
288
|
+
this.randomer._state1L | 0,
|
|
289
|
+
],
|
|
239
290
|
];
|
|
240
|
-
}
|
|
291
|
+
}
|
|
241
292
|
|
|
242
|
-
|
|
243
|
-
|
|
293
|
+
/**
|
|
294
|
+
* Resumes from a snapshot.
|
|
295
|
+
*/
|
|
296
|
+
resume(snapshot) {
|
|
244
297
|
this.render.clear();
|
|
245
298
|
this.flush();
|
|
246
299
|
this.label = this.start;
|
|
@@ -248,30 +301,30 @@ Engine.prototype.resume = function resume(snapshot) {
|
|
|
248
301
|
this.global = new Global(this.handler);
|
|
249
302
|
this.top = this.global;
|
|
250
303
|
if (snapshot == null) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
304
|
+
if (this.handler && this.handler.waypoint) {
|
|
305
|
+
this.handler.waypoint(null, this);
|
|
306
|
+
}
|
|
307
|
+
this.continue();
|
|
308
|
+
return;
|
|
256
309
|
}
|
|
257
310
|
|
|
258
311
|
// Destructure snapshot
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
312
|
+
const label = this.labelOfIndex(snapshot[0]);
|
|
313
|
+
const stack = snapshot[1];
|
|
314
|
+
const global = snapshot[2];
|
|
315
|
+
const random = snapshot[3];
|
|
263
316
|
|
|
264
317
|
// Restore globals
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
for (
|
|
268
|
-
|
|
318
|
+
const keys = global[0];
|
|
319
|
+
const values = global[1];
|
|
320
|
+
for (let i = 0; i < keys.length; i++) {
|
|
321
|
+
this.global.set(keys[i], values[i]);
|
|
269
322
|
}
|
|
270
323
|
|
|
271
324
|
// Restore stack
|
|
272
|
-
|
|
325
|
+
const engine = this;
|
|
273
326
|
this.top = stack.reduceRight(function (parent, snapshot) {
|
|
274
|
-
|
|
327
|
+
return Frame.restore(engine, snapshot, parent);
|
|
275
328
|
}, this.global);
|
|
276
329
|
|
|
277
330
|
// Restore prng
|
|
@@ -280,72 +333,86 @@ Engine.prototype.resume = function resume(snapshot) {
|
|
|
280
333
|
this.randomer._state1U = random[2];
|
|
281
334
|
this.randomer._state1L = random[3];
|
|
282
335
|
|
|
283
|
-
|
|
336
|
+
const instruction = this.story[label];
|
|
284
337
|
if (instruction.type === 'opt') {
|
|
285
|
-
|
|
286
|
-
this.flush();
|
|
287
|
-
this.continue();
|
|
288
|
-
}
|
|
289
|
-
} else {
|
|
290
|
-
this.label = label;
|
|
338
|
+
if (this.gothrough(instruction.answer, 'RET')) {
|
|
291
339
|
this.flush();
|
|
292
340
|
this.continue();
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
this.label = label;
|
|
344
|
+
this.flush();
|
|
345
|
+
this.continue();
|
|
293
346
|
}
|
|
294
|
-
}
|
|
347
|
+
}
|
|
295
348
|
|
|
296
|
-
|
|
297
|
-
Engine.prototype.log = function log() {
|
|
349
|
+
log() {
|
|
298
350
|
this.top.log();
|
|
299
351
|
console.log('');
|
|
300
|
-
}
|
|
352
|
+
}
|
|
301
353
|
|
|
302
|
-
|
|
354
|
+
labelOfIndex(index) {
|
|
355
|
+
if (index == -2) {
|
|
356
|
+
return 'RET';
|
|
357
|
+
} else if (index === -3) {
|
|
358
|
+
return 'ESC';
|
|
359
|
+
}
|
|
360
|
+
return this.labels[index];
|
|
361
|
+
}
|
|
303
362
|
|
|
304
|
-
|
|
363
|
+
indexOfLabel(label) {
|
|
364
|
+
if (label === 'RET') {
|
|
365
|
+
return -2;
|
|
366
|
+
} else if (label === 'ESC') {
|
|
367
|
+
return -3;
|
|
368
|
+
}
|
|
369
|
+
return this.labels.indexOf(label);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Here begin the instructions
|
|
373
|
+
|
|
374
|
+
$text() {
|
|
305
375
|
return this.write(this.instruction.text);
|
|
306
|
-
}
|
|
376
|
+
}
|
|
307
377
|
|
|
308
|
-
|
|
309
|
-
return this.write(
|
|
310
|
-
}
|
|
378
|
+
$echo() {
|
|
379
|
+
return this.write(`${evaluate(this.top, this.randomer, this.instruction.expression)}`);
|
|
380
|
+
}
|
|
311
381
|
|
|
312
|
-
|
|
382
|
+
$br() {
|
|
313
383
|
this.render.break();
|
|
314
384
|
return this.goto(this.instruction.next);
|
|
315
|
-
}
|
|
385
|
+
}
|
|
316
386
|
|
|
317
|
-
|
|
387
|
+
$par() {
|
|
318
388
|
this.render.paragraph();
|
|
319
389
|
return this.goto(this.instruction.next);
|
|
320
|
-
}
|
|
390
|
+
}
|
|
321
391
|
|
|
322
|
-
|
|
392
|
+
$rule() {
|
|
323
393
|
// TODO
|
|
324
394
|
this.render.paragraph();
|
|
325
395
|
return this.goto(this.instruction.next);
|
|
326
|
-
}
|
|
396
|
+
}
|
|
327
397
|
|
|
328
|
-
|
|
398
|
+
$goto() {
|
|
329
399
|
return this.goto(this.instruction.next);
|
|
330
|
-
}
|
|
400
|
+
}
|
|
331
401
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
// istanbul ignore if
|
|
402
|
+
$call() {
|
|
403
|
+
const label = this.instruction.label;
|
|
404
|
+
const def = this.story[label];
|
|
336
405
|
if (!def) {
|
|
337
|
-
|
|
338
|
-
|
|
406
|
+
console.error(`no such procedure ${label}`, this.instruction);
|
|
407
|
+
return this.resume();
|
|
339
408
|
}
|
|
340
|
-
// istanbul ignore if
|
|
341
409
|
if (def.type !== 'def') {
|
|
342
|
-
|
|
343
|
-
|
|
410
|
+
console.error(`Can't call non-procedure ${label}`, this.instruction);
|
|
411
|
+
return this.resume();
|
|
344
412
|
}
|
|
345
|
-
// istanbul ignore if
|
|
346
413
|
if (def.locals.length !== this.instruction.args.length) {
|
|
347
|
-
|
|
348
|
-
|
|
414
|
+
console.error(`Argument length mismatch for ${label}`, this.instruction);
|
|
415
|
+
return this.resume();
|
|
349
416
|
}
|
|
350
417
|
// TODO replace this.global with closure scope if scoped procedures become
|
|
351
418
|
// viable. This will require that the engine create references to closures
|
|
@@ -353,210 +420,214 @@ Engine.prototype.$call = function $call() {
|
|
|
353
420
|
// capturing locals. As such the parser will need to retain a reference to
|
|
354
421
|
// the enclosing procedure and note all of the child procedures as they are
|
|
355
422
|
// encountered.
|
|
356
|
-
this.top = new Frame(
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
423
|
+
this.top = new Frame(
|
|
424
|
+
this.top,
|
|
425
|
+
def.locals,
|
|
426
|
+
this.instruction.next,
|
|
427
|
+
this.instruction.branch,
|
|
428
|
+
this.label
|
|
429
|
+
);
|
|
430
|
+
for (let i = 0; i < this.instruction.args.length; i++) {
|
|
431
|
+
const arg = this.instruction.args[i];
|
|
432
|
+
const value = evaluate(this.top.parent, this.randomer, arg);
|
|
433
|
+
this.top.set(def.locals[i], value);
|
|
361
434
|
}
|
|
362
435
|
return this.goto(label);
|
|
363
|
-
}
|
|
436
|
+
}
|
|
364
437
|
|
|
365
|
-
|
|
438
|
+
$def() {
|
|
366
439
|
// Procedure argument instructions exist as targets for labels as well as
|
|
367
440
|
// for reference to locals in calls.
|
|
368
441
|
return this.goto(this.instruction.next);
|
|
369
|
-
}
|
|
442
|
+
}
|
|
370
443
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
for (
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
444
|
+
$opt() {
|
|
445
|
+
const closure = new Closure(this.top, this.label);
|
|
446
|
+
for (let i = 0; i < this.instruction.keywords.length; i++) {
|
|
447
|
+
const keyword = this.instruction.keywords[i];
|
|
448
|
+
// The first option to introduce a keyword wins, not the last.
|
|
449
|
+
if (!this.keywords[keyword]) {
|
|
450
|
+
this.keywords[keyword] = closure;
|
|
451
|
+
}
|
|
379
452
|
}
|
|
380
453
|
if (this.instruction.question.length > 0) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
454
|
+
this.options.push(closure);
|
|
455
|
+
this.render.startOption();
|
|
456
|
+
this.top = new Frame(this.top, [], this.instruction.next, 'RET', this.label, true);
|
|
457
|
+
return this.gothrough(this.instruction.question, 'RET');
|
|
385
458
|
} else if (this.noOption == null) {
|
|
386
|
-
|
|
459
|
+
this.noOption = closure;
|
|
387
460
|
}
|
|
388
461
|
return this.goto(this.instruction.next);
|
|
389
|
-
}
|
|
462
|
+
}
|
|
390
463
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
// istanbul ignore if
|
|
464
|
+
$move() {
|
|
465
|
+
const value = evaluate(this.top, this.randomer, this.instruction.source);
|
|
466
|
+
const name = evaluate.nominate(this.top, this.randomer, this.instruction.target);
|
|
395
467
|
if (this.debug) {
|
|
396
|
-
|
|
468
|
+
console.log(`${this.top.at()}/${this.label} ${name} = ${value}`);
|
|
397
469
|
}
|
|
398
470
|
this.top.set(name, value);
|
|
399
471
|
return this.goto(this.instruction.next);
|
|
400
|
-
}
|
|
472
|
+
}
|
|
401
473
|
|
|
402
|
-
|
|
403
|
-
|
|
474
|
+
$jump() {
|
|
475
|
+
const j = this.instruction;
|
|
404
476
|
if (evaluate(this.top, this.randomer, j.condition)) {
|
|
405
|
-
|
|
477
|
+
return this.goto(this.instruction.branch);
|
|
406
478
|
} else {
|
|
407
|
-
|
|
479
|
+
return this.goto(this.instruction.next);
|
|
408
480
|
}
|
|
409
|
-
}
|
|
481
|
+
}
|
|
410
482
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
483
|
+
$switch() {
|
|
484
|
+
const branches = this.instruction.branches.slice();
|
|
485
|
+
const weightExpressions = this.instruction.weights.slice();
|
|
486
|
+
let samples = 1;
|
|
487
|
+
const nexts = [];
|
|
416
488
|
if (this.instruction.mode === 'pick') {
|
|
417
|
-
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
} else {
|
|
427
|
-
value = pick(weights, weight, this.randomer);
|
|
428
|
-
if (value == null) {
|
|
429
|
-
break;
|
|
430
|
-
}
|
|
431
|
-
}
|
|
489
|
+
samples = evaluate(this.top, this.randomer, this.instruction.expression);
|
|
490
|
+
}
|
|
491
|
+
let value, next;
|
|
492
|
+
for (let i = 0; i < samples; i++) {
|
|
493
|
+
const weights = [];
|
|
494
|
+
const weight = weigh(this.top, this.randomer, weightExpressions, weights);
|
|
495
|
+
if (this.instruction.mode === 'rand' || this.instruction.mode === 'pick') {
|
|
496
|
+
if (weights.length === weight) {
|
|
497
|
+
value = Math.floor(this.randomer.random() * branches.length);
|
|
432
498
|
} else {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
499
|
+
value = pick(weights, weight, this.randomer);
|
|
500
|
+
if (value == null) {
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
437
503
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
value = evaluate.hash(value) % branches.length;
|
|
504
|
+
} else {
|
|
505
|
+
value = evaluate(this.top, this.randomer, this.instruction.expression);
|
|
506
|
+
if (this.instruction.variable != null) {
|
|
507
|
+
this.top.set(this.instruction.variable, value + this.instruction.value);
|
|
443
508
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
509
|
+
}
|
|
510
|
+
if (this.instruction.mode === 'loop') {
|
|
511
|
+
// actual modulo, wraps negatives
|
|
512
|
+
value = ((value % branches.length) + branches.length) % branches.length;
|
|
513
|
+
} else if (this.instruction.mode === 'hash') {
|
|
514
|
+
value = evaluate.hash(value) % branches.length;
|
|
515
|
+
}
|
|
516
|
+
value = Math.min(value, branches.length - 1);
|
|
517
|
+
value = Math.max(value, 0);
|
|
518
|
+
next = branches[value];
|
|
519
|
+
pop(branches, value);
|
|
520
|
+
pop(weightExpressions, value);
|
|
521
|
+
nexts.push(next);
|
|
522
|
+
}
|
|
452
523
|
if (this.debug) {
|
|
453
|
-
|
|
524
|
+
console.log(`${this.top.at()}/${this.label} ${value} -> ${next}`);
|
|
454
525
|
}
|
|
455
526
|
return this.gothrough(nexts, this.instruction.next);
|
|
456
|
-
}
|
|
527
|
+
}
|
|
457
528
|
|
|
458
|
-
|
|
529
|
+
$cue() {
|
|
459
530
|
if (this.handler != null && this.handler.cue != null) {
|
|
460
|
-
|
|
531
|
+
return this.handler.cue(this.instruction.cue, this.instruction.next, this);
|
|
461
532
|
} else {
|
|
462
|
-
|
|
463
|
-
}
|
|
464
|
-
};
|
|
465
|
-
|
|
466
|
-
function weigh(scope, randomer, expressions, weights) {
|
|
467
|
-
var weight = 0;
|
|
468
|
-
for (var i = 0; i < expressions.length; i++) {
|
|
469
|
-
weights[i] = evaluate(scope, randomer, expressions[i]);
|
|
470
|
-
weight += weights[i];
|
|
471
|
-
}
|
|
472
|
-
return weight;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
function pick(weights, weight, randomer) {
|
|
476
|
-
var offset = Math.floor(randomer.random() * weight);
|
|
477
|
-
var passed = 0;
|
|
478
|
-
for (var i = 0; i < weights.length; i++) {
|
|
479
|
-
passed += weights[i];
|
|
480
|
-
if (offset < passed) {
|
|
481
|
-
return i;
|
|
482
|
-
}
|
|
533
|
+
return this.goto(this.instruction.next);
|
|
483
534
|
}
|
|
484
|
-
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function pop(array, index) {
|
|
488
|
-
array[index] = array[array.length - 1];
|
|
489
|
-
array.length--;
|
|
490
|
-
}
|
|
535
|
+
}
|
|
491
536
|
|
|
492
|
-
|
|
537
|
+
$ask() {
|
|
493
538
|
this.ask();
|
|
494
539
|
return false;
|
|
495
|
-
}
|
|
540
|
+
}
|
|
496
541
|
|
|
497
|
-
|
|
542
|
+
$read() {
|
|
498
543
|
this.read();
|
|
499
544
|
return false;
|
|
500
|
-
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
501
547
|
|
|
502
|
-
|
|
548
|
+
class Global {
|
|
549
|
+
constructor(handler) {
|
|
503
550
|
this.scope = Object.create(null);
|
|
504
551
|
this.handler = handler;
|
|
505
552
|
this.next = 'RET';
|
|
506
553
|
this.branch = 'RET';
|
|
507
554
|
Object.seal(this);
|
|
508
|
-
}
|
|
555
|
+
}
|
|
509
556
|
|
|
510
|
-
|
|
557
|
+
get(name) {
|
|
511
558
|
if (this.handler && this.handler.has && this.handler.has(name)) {
|
|
512
|
-
|
|
559
|
+
return this.handler.get(name);
|
|
513
560
|
} else {
|
|
514
|
-
|
|
561
|
+
return this.scope[name] || 0;
|
|
515
562
|
}
|
|
516
|
-
}
|
|
563
|
+
}
|
|
517
564
|
|
|
518
|
-
|
|
565
|
+
set(name, value) {
|
|
519
566
|
if (this.handler && this.handler.has && this.handler.has(name)) {
|
|
520
|
-
|
|
567
|
+
this.handler.set(name, value);
|
|
521
568
|
} else {
|
|
522
|
-
|
|
569
|
+
this.scope[name] = value;
|
|
523
570
|
}
|
|
524
571
|
if (this.handler && this.handler.changed) {
|
|
525
|
-
|
|
572
|
+
this.handler.changed(name, value);
|
|
526
573
|
}
|
|
527
|
-
}
|
|
574
|
+
}
|
|
528
575
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
var names = Object.keys(this.scope);
|
|
576
|
+
log() {
|
|
577
|
+
const names = Object.keys(this.scope);
|
|
532
578
|
names.sort();
|
|
533
|
-
for (
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
579
|
+
for (let i = 0; i < names.length; i++) {
|
|
580
|
+
const name = names[i];
|
|
581
|
+
const value = this.scope[name];
|
|
582
|
+
console.log(`${name} = ${value}`);
|
|
537
583
|
}
|
|
538
584
|
console.log('');
|
|
539
|
-
}
|
|
585
|
+
}
|
|
540
586
|
|
|
541
|
-
|
|
542
|
-
Global.prototype.at = function at() {
|
|
587
|
+
at() {
|
|
543
588
|
return '';
|
|
544
|
-
}
|
|
589
|
+
}
|
|
545
590
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
for (
|
|
550
|
-
|
|
591
|
+
capture() {
|
|
592
|
+
const names = Object.keys(this.scope);
|
|
593
|
+
const values = [];
|
|
594
|
+
for (let i = 0; i < names.length; i++) {
|
|
595
|
+
values[i] = this.scope[names[i]] || 0;
|
|
551
596
|
}
|
|
552
597
|
return [names, values];
|
|
553
|
-
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
class Frame {
|
|
602
|
+
static restore(engine, snapshot, parent) {
|
|
603
|
+
const label = engine.labelOfIndex(snapshot[0]);
|
|
604
|
+
const next = engine.labelOfIndex(snapshot[1]);
|
|
605
|
+
const branch = engine.labelOfIndex(snapshot[2]);
|
|
606
|
+
const values = snapshot[3];
|
|
607
|
+
const stopOption = Boolean(snapshot[4]);
|
|
608
|
+
|
|
609
|
+
const frame = new Frame(parent, [], next, branch, label, stopOption);
|
|
554
610
|
|
|
555
|
-
|
|
611
|
+
// Technically, not all frames correspond to subroutine calls, but all
|
|
612
|
+
// frames that remain when the engine pauses ought to be.
|
|
613
|
+
// The exceptions would be interstitial frames generated by gothrough,
|
|
614
|
+
// but all of these are exhausted before the engine stops to ask a prompt.
|
|
615
|
+
const call = engine.story[label];
|
|
616
|
+
const def = engine.story[call.label];
|
|
617
|
+
frame.locals = def.locals;
|
|
618
|
+
for (let i = 0; i < values.length; i++) {
|
|
619
|
+
const name = def.locals[i];
|
|
620
|
+
frame.scope[name] = values[i];
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return frame;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
constructor(parent, locals, next, branch, label, stopOption) {
|
|
556
627
|
this.locals = locals;
|
|
557
628
|
this.scope = Object.create(null);
|
|
558
|
-
for (
|
|
559
|
-
|
|
629
|
+
for (let i = 0; i < locals.length; i++) {
|
|
630
|
+
this.scope[locals[i]] = 0;
|
|
560
631
|
}
|
|
561
632
|
this.parent = parent;
|
|
562
633
|
this.next = next;
|
|
@@ -564,100 +635,58 @@ function Frame(parent, locals, next, branch, label, stopOption) {
|
|
|
564
635
|
this.label = label;
|
|
565
636
|
this.stopOption = stopOption || false;
|
|
566
637
|
Object.seal(this);
|
|
567
|
-
}
|
|
638
|
+
}
|
|
568
639
|
|
|
569
|
-
|
|
640
|
+
get(name) {
|
|
570
641
|
if (this.locals.indexOf(name) >= 0) {
|
|
571
|
-
|
|
642
|
+
return this.scope[name];
|
|
572
643
|
}
|
|
573
644
|
return this.parent.get(name);
|
|
574
|
-
}
|
|
645
|
+
}
|
|
575
646
|
|
|
576
|
-
|
|
577
|
-
// istanbul ignore else
|
|
647
|
+
set(name, value) {
|
|
578
648
|
if (this.locals.indexOf(name) >= 0) {
|
|
579
|
-
|
|
580
|
-
|
|
649
|
+
this.scope[name] = value;
|
|
650
|
+
return;
|
|
581
651
|
}
|
|
582
652
|
this.parent.set(name, value);
|
|
583
|
-
}
|
|
653
|
+
}
|
|
584
654
|
|
|
585
|
-
|
|
586
|
-
Frame.prototype.log = function log() {
|
|
655
|
+
log() {
|
|
587
656
|
this.parent.log();
|
|
588
|
-
console.log(
|
|
589
|
-
for (
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
657
|
+
console.log(`--- ${this.label} -> ${this.next}`);
|
|
658
|
+
for (let i = 0; i < this.locals.length; i++) {
|
|
659
|
+
const name = this.locals[i];
|
|
660
|
+
const value = this.scope[name];
|
|
661
|
+
console.log(`${name} = ${value}`);
|
|
593
662
|
}
|
|
594
|
-
}
|
|
663
|
+
}
|
|
595
664
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
};
|
|
665
|
+
at() {
|
|
666
|
+
return `${this.parent.at()}/${this.label}`;
|
|
667
|
+
}
|
|
600
668
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
for (
|
|
604
|
-
|
|
605
|
-
|
|
669
|
+
capture(engine) {
|
|
670
|
+
const values = [];
|
|
671
|
+
for (let i = 0; i < this.locals.length; i++) {
|
|
672
|
+
const local = this.locals[i];
|
|
673
|
+
values.push(this.scope[local] || 0);
|
|
606
674
|
}
|
|
607
675
|
|
|
608
676
|
return [
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
677
|
+
engine.indexOfLabel(this.label),
|
|
678
|
+
engine.indexOfLabel(this.next),
|
|
679
|
+
engine.indexOfLabel(this.branch),
|
|
680
|
+
values,
|
|
681
|
+
+this.stopOption,
|
|
614
682
|
];
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
Frame.restore = function (engine, snapshot, parent) {
|
|
618
|
-
var label = engine.labelOfIndex(snapshot[0]);
|
|
619
|
-
var next = engine.labelOfIndex(snapshot[1]);
|
|
620
|
-
var branch = engine.labelOfIndex(snapshot[2]);
|
|
621
|
-
var values = snapshot[3];
|
|
622
|
-
var stopOption = Boolean(snapshot[4]);
|
|
623
|
-
|
|
624
|
-
var frame = new Frame(parent, [], next, branch, label, stopOption);
|
|
625
|
-
|
|
626
|
-
// Technically, not all frames correspond to subroutine calls, but all
|
|
627
|
-
// frames that remain when the engine pauses ought to be.
|
|
628
|
-
// The exceptions would be interstitial frames generated by gothrough,
|
|
629
|
-
// but all of these are exhausted before the engine stops to ask a prompt.
|
|
630
|
-
var call = engine.story[label];
|
|
631
|
-
var def = engine.story[call.label];
|
|
632
|
-
frame.locals = def.locals;
|
|
633
|
-
for (var i = 0; i < values.length; i++) {
|
|
634
|
-
var name = def.locals[i];
|
|
635
|
-
frame.scope[name] = values[i];
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
return frame;
|
|
639
|
-
};
|
|
640
|
-
|
|
641
|
-
Engine.prototype.labelOfIndex = function (index) {
|
|
642
|
-
if (index == -2) {
|
|
643
|
-
return 'RET';
|
|
644
|
-
} else if (index === -3) {
|
|
645
|
-
return 'ESC';
|
|
646
|
-
}
|
|
647
|
-
return this.labels[index];
|
|
648
|
-
};
|
|
649
|
-
|
|
650
|
-
Engine.prototype.indexOfLabel = function (label) {
|
|
651
|
-
if (label === 'RET') {
|
|
652
|
-
return -2;
|
|
653
|
-
} else if (label === 'ESC') {
|
|
654
|
-
return -3;
|
|
655
|
-
}
|
|
656
|
-
return this.labels.indexOf(label);
|
|
657
|
-
};
|
|
683
|
+
}
|
|
684
|
+
}
|
|
658
685
|
|
|
659
|
-
|
|
686
|
+
class Closure {
|
|
687
|
+
constructor(scope, label) {
|
|
660
688
|
this.scope = scope;
|
|
661
689
|
this.label = label;
|
|
662
690
|
Object.seal(this);
|
|
691
|
+
}
|
|
663
692
|
}
|