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/engine.js CHANGED
@@ -1,246 +1,299 @@
1
- 'use strict';
1
+ import evaluate from './evaluate.js';
2
+ import describe from './describe.js';
2
3
 
3
- var Story = require('./story');
4
- var evaluate = require('./evaluate');
5
- var describe = require('./describe');
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
- module.exports = Engine;
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
- var debug = typeof process === 'object' && process.env.DEBUG_ENGINE;
30
+ export default class Engine {
31
+ debug = typeof process === 'object' && process.env.DEBUG_ENGINE;
10
32
 
11
- function Engine(args) {
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 = new Story.constructors.goto(this.start);
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
- Engine.prototype.reset = function reset() {
37
- Engine.call(this, this);
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
- Engine.prototype.continue = function _continue() {
42
- var _continue;
43
- do {
44
- // istanbul ignore if
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
- Engine.prototype.goto = function _goto(label) {
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
- // istanbul ignore if
67
- if (this.debug) {
68
- console.log(label.toLowerCase());
69
- }
70
- if (this.top.stopOption) {
71
- this.render.stopOption();
72
- }
73
- if (label === 'ESC') {
74
- label = this.top.branch;
75
- } else {
76
- label = this.top.next;
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
- return this.end();
133
+ return this.end();
83
134
  }
84
135
 
85
- var next = this.story[label];
86
- // istanbul ignore if
136
+ const next = this.story[label];
87
137
  if (!next) {
88
- console.error('Story missing label', label);
89
- return this.resume();
138
+ console.error('Story missing label', label);
139
+ return this.resume();
90
140
  }
91
- // istanbul ignore if
92
141
  if (!next) {
93
- console.error('Story missing instruction for label: ' + label);
94
- return this.resume();
142
+ console.error(`Story missing instruction for label: ${label}`);
143
+ return this.resume();
95
144
  }
96
145
  if (this.handler && this.handler.goto) {
97
- this.handler.goto(label, next);
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
- Engine.prototype.gothrough = function gothrough(sequence, next) {
105
- var prev = this.label;
106
- for (var i = sequence.length - 1; i >= 0; i--) {
107
- if (next !== 'RET') {
108
- this.top = new Frame(this.top, [], next, 'RET', prev);
109
- }
110
- prev = next;
111
- next = sequence[i];
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
- Engine.prototype.end = function end() {
165
+ end() {
166
+ this.display();
117
167
  if (this.handler && this.handler.end) {
118
- this.handler.end(this);
168
+ this.handler.end(this);
119
169
  }
120
- this.display();
121
170
  this.dialog.close();
122
171
  return false;
123
- };
172
+ }
124
173
 
125
- Engine.prototype.ask = function ask() {
174
+ ask() {
126
175
  if (this.options.length) {
127
- this.display();
128
- if (this.handler && this.handler.ask) {
129
- this.handler.ask(this);
130
- }
131
- this.dialog.ask();
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
- var closure = this.noOption;
134
- var option = this.story[closure.label];
135
- this.top = closure.scope;
136
- var answer = option.answer;
137
- this.flush();
138
- this.gothrough(answer, 'RET');
139
- this.continue();
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
- return this.goto('RET');
190
+ return this.goto('RET');
142
191
  }
143
- };
192
+ }
144
193
 
145
- Engine.prototype.read = function read() {
194
+ read() {
146
195
  this.display();
147
196
  if (this.handler && this.handler.ask) {
148
- this.handler.ask(this);
197
+ this.handler.ask(this);
149
198
  }
150
199
  this.dialog.ask(this.instruction.cue);
151
- };
200
+ }
152
201
 
153
- Engine.prototype.answer = function answer(text) {
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
- this.handler.answer(text, this);
208
+ this.handler.answer(text, this);
156
209
  }
157
210
  this.render.flush();
158
211
  if (this.instruction.type === 'read') {
159
- this.top.set(this.instruction.variable, text);
160
- this.render.clear();
161
- if (this.goto(this.instruction.next)) {
162
- this.continue();
163
- }
164
- return;
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
- var choice = text - 1;
219
+ const choice = text - 1;
167
220
  if (choice >= 0 && choice < this.options.length) {
168
- return this.choice(this.options[choice]);
221
+ return this.choice(this.options[choice]);
169
222
  } else if (this.keywords[text]) {
170
- return this.choice(this.keywords[text]);
223
+ return this.choice(this.keywords[text]);
171
224
  } else {
172
- this.render.pardon();
173
- this.ask();
225
+ this.render.pardon();
226
+ this.ask();
174
227
  }
175
- };
228
+ }
176
229
 
177
- Engine.prototype.choice = function _choice(closure) {
178
- var option = this.story[closure.label];
230
+ choice(closure) {
231
+ const option = this.story[closure.label];
179
232
  if (this.handler && this.handler.choice) {
180
- this.handler.choice(option, this);
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
- this.handler.waypoint(this.waypoint, this);
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
- this.flush();
194
- this.continue();
245
+ this.flush();
246
+ this.continue();
195
247
  }
196
- };
248
+ }
197
249
 
198
- Engine.prototype.display = function display() {
250
+ display() {
199
251
  this.render.display();
200
- };
252
+ }
201
253
 
202
- Engine.prototype.flush = function flush() {
254
+ flush() {
203
255
  this.options.length = 0;
204
256
  this.noOption = null;
205
257
  this.keywords = {};
206
- };
258
+ }
207
259
 
208
- Engine.prototype.write = function write(text) {
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
- // istanbul ignore next
214
- Engine.prototype.capture = function capture(closure) {
215
- var label, top;
265
+ capture(closure) {
266
+ let label, top;
216
267
  if (closure != null) {
217
- label = closure.label;
218
- top = closure.scope;
268
+ label = closure.label;
269
+ top = closure.scope;
219
270
  } else {
220
- label = this.label;
221
- top = this.top;
271
+ label = this.label;
272
+ top = this.top;
222
273
  }
223
274
 
224
- var stack = [];
275
+ const stack = [];
225
276
  for (; top != this.global; top = top.parent) {
226
- stack.push(top.capture(this));
277
+ stack.push(top.capture(this));
227
278
  }
228
279
 
229
280
  return [
230
- this.indexOfLabel(label),
231
- stack,
232
- this.global.capture(),
233
- [
234
- this.randomer._state0U | 0,
235
- this.randomer._state0L | 0,
236
- this.randomer._state1U | 0,
237
- this.randomer._state1L | 0
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
- // istanbul ignore next
243
- Engine.prototype.resume = function resume(snapshot) {
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
- if (this.handler && this.handler.waypoint) {
252
- this.handler.waypoint(null, this);
253
- }
254
- this.continue();
255
- return;
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
- var label = this.labelOfIndex(snapshot[0]);
260
- var stack = snapshot[1];
261
- var global = snapshot[2];
262
- var random = snapshot[3];
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
- var keys = global[0];
266
- var values = global[1];
267
- for (var i = 0; i < keys.length; i++) {
268
- this.global.set(keys[i], values[i]);
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
- var engine = this;
325
+ const engine = this;
273
326
  this.top = stack.reduceRight(function (parent, snapshot) {
274
- return Frame.restore(engine, snapshot, parent);
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
- var instruction = this.story[label];
336
+ const instruction = this.story[label];
284
337
  if (instruction.type === 'opt') {
285
- if (this.gothrough(instruction.answer, 'RET')) {
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
- // istanbul ignore next
297
- Engine.prototype.log = function log() {
349
+ log() {
298
350
  this.top.log();
299
351
  console.log('');
300
- };
352
+ }
301
353
 
302
- // Here begin the instructions
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
- Engine.prototype.$text = function $text() {
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
- Engine.prototype.$echo = function $echo() {
309
- return this.write('' + evaluate(this.top, this.randomer, this.instruction.expression));
310
- };
378
+ $echo() {
379
+ return this.write(`${evaluate(this.top, this.randomer, this.instruction.expression)}`);
380
+ }
311
381
 
312
- Engine.prototype.$br = function $br() {
382
+ $br() {
313
383
  this.render.break();
314
384
  return this.goto(this.instruction.next);
315
- };
385
+ }
316
386
 
317
- Engine.prototype.$par = function $par() {
387
+ $par() {
318
388
  this.render.paragraph();
319
389
  return this.goto(this.instruction.next);
320
- };
390
+ }
321
391
 
322
- Engine.prototype.$rule = function $rule() {
392
+ $rule() {
323
393
  // TODO
324
394
  this.render.paragraph();
325
395
  return this.goto(this.instruction.next);
326
- };
396
+ }
327
397
 
328
- Engine.prototype.$goto = function $goto() {
398
+ $goto() {
329
399
  return this.goto(this.instruction.next);
330
- };
400
+ }
331
401
 
332
- Engine.prototype.$call = function $call() {
333
- var label = this.instruction.label;
334
- var def = this.story[label];
335
- // istanbul ignore if
402
+ $call() {
403
+ const label = this.instruction.label;
404
+ const def = this.story[label];
336
405
  if (!def) {
337
- console.error('no such procedure ' + label, this.instruction);
338
- return this.resume();
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
- console.error('Can\'t call non-procedure ' + label, this.instruction);
343
- return this.resume();
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
- console.error('Argument length mismatch for ' + label, this.instruction, procedure);
348
- return this.resume();
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(this.top, def.locals, this.instruction.next, this.instruction.branch, this.label);
357
- for (var i = 0; i < this.instruction.args.length; i++) {
358
- var arg = this.instruction.args[i];
359
- var value = evaluate(this.top.parent, this.randomer, arg);
360
- this.top.set(def.locals[i], value);
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
- Engine.prototype.$def = function $def() {
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
- Engine.prototype.$opt = function $opt() {
372
- var closure = new Closure(this.top, this.label);
373
- for (var i = 0; i < this.instruction.keywords.length; i++) {
374
- var keyword = this.instruction.keywords[i];
375
- // The first option to introduce a keyword wins, not the last.
376
- if (!this.keywords[keyword]) {
377
- this.keywords[keyword] = closure;
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
- this.options.push(closure);
382
- this.render.startOption();
383
- this.top = new Frame(this.top, [], this.instruction.next, 'RET', this.label, true);
384
- return this.gothrough(this.instruction.question, 'RET');
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
- this.noOption = closure;
459
+ this.noOption = closure;
387
460
  }
388
461
  return this.goto(this.instruction.next);
389
- };
462
+ }
390
463
 
391
- Engine.prototype.$move = function $move() {
392
- var value = evaluate(this.top, this.randomer, this.instruction.source);
393
- var name = evaluate.nominate(this.top, this.randomer, this.instruction.target);
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
- console.log(this.top.at() + '/' + this.label + ' ' + name + ' = ' + value);
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
- Engine.prototype.$jump = function $jump() {
403
- var j = this.instruction;
474
+ $jump() {
475
+ const j = this.instruction;
404
476
  if (evaluate(this.top, this.randomer, j.condition)) {
405
- return this.goto(this.instruction.branch);
477
+ return this.goto(this.instruction.branch);
406
478
  } else {
407
- return this.goto(this.instruction.next);
479
+ return this.goto(this.instruction.next);
408
480
  }
409
- };
481
+ }
410
482
 
411
- Engine.prototype.$switch = function $switch() {
412
- var branches = this.instruction.branches.slice();
413
- var weightExpressions = this.instruction.weights.slice();
414
- var samples = 1;
415
- var nexts = [];
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
- samples = evaluate(this.top, this.randomer, this.instruction.expression);
418
- }
419
- for (var i = 0; i < samples; i++) {
420
- var value;
421
- var weights = [];
422
- var weight = weigh(this.top, this.randomer, weightExpressions, weights);
423
- if (this.instruction.mode === 'rand' || this.instruction.mode === 'pick') {
424
- if (weights.length === weight) {
425
- value = Math.floor(this.randomer.random() * branches.length);
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
- value = evaluate(this.top, this.randomer, this.instruction.expression);
434
- if (this.instruction.variable != null) {
435
- this.top.set(this.instruction.variable, value + this.instruction.value);
436
- }
499
+ value = pick(weights, weight, this.randomer);
500
+ if (value == null) {
501
+ break;
502
+ }
437
503
  }
438
- if (this.instruction.mode === 'loop') {
439
- // actual modulo, wraps negatives
440
- value = ((value % branches.length) + branches.length) % branches.length;
441
- } else if (this.instruction.mode === 'hash') {
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
- value = Math.min(value, branches.length - 1);
445
- value = Math.max(value, 0);
446
- var next = branches[value];
447
- pop(branches, value);
448
- pop(weightExpressions, value);
449
- nexts.push(next);
450
- }
451
- // istanbul ignore if
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
- console.log(this.top.at() + '/' + this.label + ' ' + value + ' -> ' + next);
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
- Engine.prototype.$cue = function $cue() {
529
+ $cue() {
459
530
  if (this.handler != null && this.handler.cue != null) {
460
- return this.handler.cue(this.instruction.cue, this.instruction.next, this);
531
+ return this.handler.cue(this.instruction.cue, this.instruction.next, this);
461
532
  } else {
462
- return this.goto(this.instruction.next);
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
- return null;
485
- }
486
-
487
- function pop(array, index) {
488
- array[index] = array[array.length - 1];
489
- array.length--;
490
- }
535
+ }
491
536
 
492
- Engine.prototype.$ask = function $ask() {
537
+ $ask() {
493
538
  this.ask();
494
539
  return false;
495
- };
540
+ }
496
541
 
497
- Engine.prototype.$read = function $read() {
542
+ $read() {
498
543
  this.read();
499
544
  return false;
500
- };
545
+ }
546
+ }
501
547
 
502
- function Global(handler) {
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
- Global.prototype.get = function get(name) {
557
+ get(name) {
511
558
  if (this.handler && this.handler.has && this.handler.has(name)) {
512
- return this.handler.get(name);
559
+ return this.handler.get(name);
513
560
  } else {
514
- return this.scope[name] || 0;
561
+ return this.scope[name] || 0;
515
562
  }
516
- };
563
+ }
517
564
 
518
- Global.prototype.set = function set(name, value) {
565
+ set(name, value) {
519
566
  if (this.handler && this.handler.has && this.handler.has(name)) {
520
- this.handler.set(name, value);
567
+ this.handler.set(name, value);
521
568
  } else {
522
- this.scope[name] = value;
569
+ this.scope[name] = value;
523
570
  }
524
571
  if (this.handler && this.handler.changed) {
525
- this.handler.changed(name, value);
572
+ this.handler.changed(name, value);
526
573
  }
527
- };
574
+ }
528
575
 
529
- // istanbul ignore next
530
- Global.prototype.log = function log() {
531
- var names = Object.keys(this.scope);
576
+ log() {
577
+ const names = Object.keys(this.scope);
532
578
  names.sort();
533
- for (var i = 0; i < names.length; i++) {
534
- var name = names[i];
535
- var value = this.scope[name];
536
- console.log(name + ' = ' + value);
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
- // istanbul ignore next
542
- Global.prototype.at = function at() {
587
+ at() {
543
588
  return '';
544
- };
589
+ }
545
590
 
546
- Global.prototype.capture = function () {
547
- var names = Object.keys(this.scope);
548
- var values = [];
549
- for (var i = 0; i < names.length; i++) {
550
- values[i] = this.scope[names[i]] || 0;
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
- function Frame(parent, locals, next, branch, label, stopOption) {
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 (var i = 0; i < locals.length; i++) {
559
- this.scope[locals[i]] = 0;
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
- Frame.prototype.get = function get(name) {
640
+ get(name) {
570
641
  if (this.locals.indexOf(name) >= 0) {
571
- return this.scope[name];
642
+ return this.scope[name];
572
643
  }
573
644
  return this.parent.get(name);
574
- };
645
+ }
575
646
 
576
- Frame.prototype.set = function set(name, value) {
577
- // istanbul ignore else
647
+ set(name, value) {
578
648
  if (this.locals.indexOf(name) >= 0) {
579
- this.scope[name] = value;
580
- return;
649
+ this.scope[name] = value;
650
+ return;
581
651
  }
582
652
  this.parent.set(name, value);
583
- };
653
+ }
584
654
 
585
- // istanbul ignore next
586
- Frame.prototype.log = function log() {
655
+ log() {
587
656
  this.parent.log();
588
- console.log('--- ' + this.label + ' -> ' + this.next);
589
- for (var i = 0; i < this.locals.length; i++) {
590
- var name = this.locals[i];
591
- var value = this.scope[name];
592
- console.log(name + ' = ' + value);
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
- // istanbul ignore next
597
- Frame.prototype.at = function at() {
598
- return this.parent.at() + '/' + this.label;
599
- };
665
+ at() {
666
+ return `${this.parent.at()}/${this.label}`;
667
+ }
600
668
 
601
- Frame.prototype.capture = function capture(engine) {
602
- var values = [];
603
- for (var i = 0; i < this.locals.length; i++) {
604
- var local = this.locals[i];
605
- values.push(this.scope[local] || 0);
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
- engine.indexOfLabel(this.label),
610
- engine.indexOfLabel(this.next),
611
- engine.indexOfLabel(this.branch),
612
- values,
613
- +this.stopOption,
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
- function Closure(scope, label) {
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
  }