-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathseaduck.js
261 lines (250 loc) · 7.29 KB
/
seaduck.js
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
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
'use strict';
let hash = require('object-hash');
let tracery = require('tracery-grammar');
function mk(t) {
return t.join("$");
}
function filterTagMatch(matchStr, item) {
if (matchStr.charAt(0) == "#") {
let tagStr = matchStr.substring(1);
if (item.tags.includes(tagStr)) {
return true;
}
}
else {
if (item.name == matchStr) {
return true;
}
}
return false;
}
class Narrative {
constructor(narrative) {
this.narrative = narrative;
this.stepCount = 0;
this.relations = new Map();
this.eventHistory = [];
this.stateHistory = [];
}
choice(t) {
// convenience function for selecting among alternatives in a list
return t[Math.floor(Math.random()*t.length)];
}
noun(name) {
// get the noun object in the narrative with the corresponding name
for (let noun of this.narrative.nouns) {
if (noun.name == name) {
return noun;
}
}
}
getNounsByTag(tag) {
// get all nouns in the narrative with this tag
let matches = [];
for (let noun of this.narrative.nouns) {
if (noun.tags.includes(tag)) {
matches.push(noun);
}
}
return matches;
}
getNounsByProperty(prop, val) {
// get all nouns with this property
let matches = [];
for (let noun of this.narrative.nouns) {
if (noun.properties[prop] == val) {
matches.push(noun);
}
}
return matches;
}
relate(rel, a, b) {
// relate a to b with relation rel
this.relations.set(mk([rel, a.name, b.name]), true)
}
unrelate(rel, a, b) {
// remove relation rel between a and b
this.relations.delete(mk([rel, a.name, b.name]));
}
unrelateByTag(rel, a, bTag) {
// remove relation rel between a and nouns tagged with bTag
for (let noun of this.allRelatedByTag(rel, a, bTag)) {
this.unrelate(rel, a, noun);
}
}
reciprocal(rel, a, b) {
// relate a to b reciprocally with relation rel
this.relations.set(mk([rel, a.name, b.name]), true)
this.relations.set(mk([rel, b.name, a.name]), true)
}
unreciprocal(rel, a, b) {
// remove reciprocal relation rel between a and b
this.relations.delete(mk([rel, a.name, b.name]))
this.relations.delete(mk([rel, b.name, a.name]))
}
unreciprocalByTag(rel, a, bTag) {
// remove reciprocal relation rel between a and nouns tagged with bTag
for (let noun of this.allRelatedByTag(rel, a, bTag)) {
this.unrelate(rel, a, noun);
this.unrelate(rel, noun, a);
}
}
isRelated(rel, a, b) {
// return true if a and b are related with rel
return this.relations.get(mk([rel, a.name, b.name]))
}
allRelatedByTag(rel, a, bTag) {
// returns all nouns related to a by rel with tag bTag
let matches = [];
let byTag = this.getNounsByTag(bTag);
for (let b of byTag) {
if (this.isRelated(rel, a, b)) {
matches.push(b);
}
}
return matches;
}
relatedByTag(rel, a, bTag) {
// returns only the first noun related to a by rel with tag bTag
return this.allRelatedByTag(rel, a, bTag)[0];
}
init() {
// call the initialize function and add events to history
let events = [];
let boundInit = this.narrative.initialize.bind(this);
for (let sEvent of boundInit()) {
this.eventHistory.push(sEvent);
events.push(sEvent);
}
return events;
}
step() {
// step through the simulation
// do nothing if story is over
if (this.eventHistory.length > 0 &&
this.eventHistory[this.eventHistory.length-1].ending()) {
return [];
}
// initialize on stepCount 0, if provided
if (this.stepCount == 0 && this.narrative.hasOwnProperty('initialize')) {
this.stepCount++;
return this.init();
}
let events = [];
// for matches with two parameters
for (let action of this.narrative.actions) {
if (action.match.length == 2) {
let matchingA = this.narrative.nouns.filter(
function(item) { return filterTagMatch(action.match[0], item); });
let matchingB = this.narrative.nouns.filter(
function(item) { return filterTagMatch(action.match[1], item); });
let boundWhen = action.when.bind(this);
let boundAction = action.action.bind(this);
for (let objA of matchingA) {
for (let objB of matchingB) {
if (objA == objB) {
continue;
}
if (boundWhen(objA, objB)) {
for (let sEvent of boundAction(objA, objB)) {
this.eventHistory.push(sEvent);
events.push(sEvent);
}
}
}
}
}
// for matches with one parameter
else if (action.match.length == 1) {
let matching = this.narrative.nouns.filter(
function(item) { return filterTagMatch(action.match[0], item); });
let boundWhen = action.when.bind(this);
let boundAction = action.action.bind(this);
for (let obj of matching) {
if (boundWhen(obj)) {
for (let sEvent of boundAction(obj)) {
this.eventHistory.push(sEvent);
events.push(sEvent);
}
}
}
}
}
// hash the current state and store
this.stateHistory.push(hash(this.narrative.nouns) + hash(this.relations));
this.stepCount++;
// if the last two states are identical, or no events generated, the end
let shLen = this.stateHistory.length;
if (
(shLen >= 2 && this.stateHistory[shLen-1] == this.stateHistory[shLen-2]) ||
events.length == 0) {
// _end is a special sentinel value to signal the end of the narration
this.eventHistory.push(new StoryEvent("_end"));
events.push(new StoryEvent("_end"));
}
return events;
}
renderEvent(ev) {
// renders an event using the associated tracery rule
let discourseCopy = JSON.parse(
JSON.stringify(this.narrative.traceryDiscourse));
if (ev.a) {
discourseCopy["nounA"] = ev.a.name;
// copy properties as nounA_<propertyname>
for (let k in ev.a.properties) {
if (ev.a.properties.hasOwnProperty(k)) {
discourseCopy["nounA_"+k] = ev.a.properties[k];
}
}
}
if (ev.b) {
discourseCopy["nounB"] = ev.b.name;
for (let k in ev.b.properties) {
if (ev.b.properties.hasOwnProperty(k)) {
discourseCopy["nounB_"+k] = ev.b.properties[k];
}
}
}
let grammar = tracery.createGrammar(discourseCopy);
grammar.addModifiers(tracery.baseEngModifiers);
return grammar.flatten("#"+ev.verb+"#");
}
stepAndRender() {
// combines step() and renderEvent()
let events = this.step();
let rendered = [];
for (let ev of events) {
rendered.push(this.renderEvent(ev));
}
return rendered;
}
}
class StoryEvent {
constructor(verb, a, b) {
this.verb = verb;
this.arity = 0;
if (a !== undefined) {
this.a = a;
this.arity++;
}
if (b !== undefined) {
this.b = b;
this.arity++;
}
}
dump() {
if (this.arity == 0) {
return [this.verb];
}
else if (this.arity == 1) {
return [this.a.name, this.verb];
}
else if (this.arity == 2) {
return [this.a.name, this.verb, this.b.name];
}
}
ending() {
return this.verb == '_end';
}
}
module.exports = {Narrative: Narrative, StoryEvent: StoryEvent};