-
Notifications
You must be signed in to change notification settings - Fork 2
/
praxish.js
338 lines (329 loc) · 14.4 KB
/
praxish.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
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
// Return a random item from a list.
function randNth(items) {
return items[Math.floor(Math.random() * items.length)];
}
// Given a text `template` to render and a map of `bindings` to swap in,
// return a copy of the `template` with all `[SquareBracketed]` variables
// replaced by the corresponding value from `bindings`.
function renderText(template, bindings) {
let outputText = template;
for (const [key, value] of Object.entries(bindings)) {
outputText = outputText.replaceAll(`[${key}]`, value);
}
return outputText;
}
// Create and return a fresh "Praxish state": a wrapper object bundling up
// all of the internal state that a Praxish simulation needs to run.
// Basically you can think of this object as "the interpreter".
// FIXME For now you need to initialize `allChars` manually
// on the returned object; see `tests.js` for an example.
function createPraxishState() {
return {db: {}, practiceDefs: {}, allChars: [], actorIdx: -1};
}
// Given a `praxishState` and a `practiceDef` defining a practice,
// update the `praxishState` to include this newly defined practice
// and return the updated `praxishState`.
function definePractice(praxishState, practiceDef) {
praxishState.practiceDefs[practiceDef.id] = practiceDef;
// Insert this practice's static data into the DB under the practiceData.PRACTICE_ID prefix.
const practiceDataPrefix = `practiceData.${practiceDef.id}.`;
const practiceData = (practiceDef.data || []).map(s => practiceDataPrefix + s);
for (const sentence of practiceData) {
insert(praxishState.db, sentence);
}
return praxishState;
}
// Given an exclusion logic `db`, a list of `conditions` to satisfy,
// and a map of previously established `bindings`, return all possible
// internally consistent bindings maps that satisfy the `conditions`.
// Use `query` instead of the simpler `unifyAll` when you need negation,
// variable equality checks, and so on in your conditions.
function query(db, conditions, bindings) {
let matches = [bindings];
for (const condition of conditions) {
const nextMatches = [];
const parts = condition.trim().split(/\s+/);
if (parts.length === 1) {
// Simple condition: just a logic sentence to try unifying with.
for (const match of matches) {
for (const newMatch of unify(condition, db, match)) {
nextMatches.push(newMatch);
}
}
}
else {
// Complex condition: an `op` plus one or more arguments.
const op = parts[0];
if (op === "not") {
// Check that the argument sentence *doesn't* unify, and kill the match if it does.
for (const match of matches) {
const badMatches = unify(parts[1], db, match);
if (badMatches.length > 0) continue; // Implicitly kill match via no-op
nextMatches.push(match);
}
}
else if (op === "eq") {
// Unify the two arguments, which can be either bound vars, unbound vars, or constants.
const [_, lhs, rhs] = parts;
for (const match of matches) {
const groundedLhs = isVariable(lhs) ? match[lhs] : lhs;
const groundedRhs = isVariable(rhs) ? match[rhs] : rhs;
if (groundedLhs && groundedRhs) {
// Both sides bound or constant. Kill the match if they're not equal.
if (groundedLhs !== groundedRhs) continue; // Implicitly kill match via no-op
nextMatches.push(match);
}
else if (groundedLhs || groundedRhs) {
// One side bound or constant, one unbound. Set the unbound var to the known val.
const unboundVar = groundedLhs ? rhs : lhs;
const groundedVal = groundedLhs ? groundedLhs : groundedRhs;
const newMatch = clone(match);
newMatch[unboundVar] = groundedVal;
nextMatches.push(newMatch);
}
else {
// Both sides unbound. Probably indicates an error in the practice definition.
console.warn("Both sides of eq check unbound", condition);
}
}
}
else if (op === "neq") {
// Kill the match if the two arguments (either constants or bound vars) are equal.
// Basically a simplified version of the `eq` logic with the equality check inverted.
const [_, lhs, rhs] = parts;
for (const match of matches) {
const groundedLhs = isVariable(lhs) ? match[lhs] : lhs;
const groundedRhs = isVariable(rhs) ? match[rhs] : rhs;
if (groundedLhs && groundedRhs) {
// Both sides bound or constant. Kill the match if they're equal.
if (groundedLhs === groundedRhs) continue; // Implicitly kill match via no-op
nextMatches.push(match);
}
else {
// At least one of the arguments is unbound.
// Probably indicates an error in the practice definition.
console.warn("Part of neq check unbound", condition);
}
}
}
else {
console.warn("Bad condition op", op, condition);
}
}
matches = nextMatches;
}
return matches;
}
// Given a `praxishState` and an `actor`, return a list of all possible actions
// that the `actor` can perform.
function getAllPossibleActions(praxishState, actor) {
const initBindings = {Actor: actor};
const allPossibleActions = [];
for (const practiceID of Object.keys(praxishState.db.practice)) {
// Query for instances of this practice.
const practiceDef = praxishState.practiceDefs[practiceID];
const prefix = `practice.${practiceID}.`;
const instancesQuery = prefix + practiceDef.roles.join(".");
const instances = unify(instancesQuery, praxishState.db, initBindings);
for (const instance of instances) {
// Get all possible actions for this actor from this instance.
const instanceID = ground(instancesQuery, instance);
for (const actionDef of practiceDef.actions) {
// Unify this action's conditions with the DB and previous bindings.
const possibleActions = query(praxishState.db, actionDef.conditions, instance);
for (const action of possibleActions) {
// Link this possible action to its originating practice definition,
// practice instance, and action definition.
action.practiceID = practiceID;
action.instanceID = instanceID;
action.actionID = actionDef.name;
// Swap variable values into the action name template.
action.name = renderText(actionDef.name, action);
// Add this possible action to the list of all possible actions.
allPossibleActions.push(action);
}
}
}
}
return allPossibleActions;
}
// Given an `outcome` string and a map of `bindings` to use for grounding any
// logic variables that appear in the `outcome`, return a fully grounded copy
// of the `outcome` (suitable for interpretation by `performOutcome`).
function groundOutcome(outcome, bindings) {
const parts = outcome.trim().split(/\s+/);
const op = parts[0];
if (op === "insert" || op === "delete") {
const sentence = parts[1];
const groundedSentence = ground(sentence, bindings);
const groundedOutcome = op + " " + groundedSentence;
return groundedOutcome;
}
else if (op === "call") {
const functionName = parts[1];
const params = parts.slice(2);
const groundedParams = params.map(param => ground(param, bindings));
const groundedOutcome = op + " " + functionName + " " + groundedParams.join(" ");
return groundedOutcome;
}
else {
return outcome;
}
}
// Given a `praxishState` and a fully grounded `outcome` (i.e., one of the
// possible consequences of an action), perform the `outcome` and return
// the updated `praxishState`.
function performOutcome(praxishState, outcome) {
const parts = outcome.trim().split(/\s+/);
const op = parts[0];
if (op === "insert") {
// First just perform the insertion.
const sentence = parts[1];
insert(praxishState.db, sentence);
// Then figure out whether we're spawning a new practice instance,
// and initialize the newly spawned instance if we are.
const sentenceParts = sentence.split(/[\.\!]/);
const practiceID = sentenceParts[0] === "practice" && sentenceParts[1];
const practiceDef = praxishState.practiceDefs[practiceID];
if (practiceID && !practiceDef) {
console.warn("Undefined practice", practiceID);
return praxishState;
}
const isSpawning = practiceDef && sentenceParts.length === practiceDef.roles.length + 2;
if (isSpawning) {
//console.log("Spawning practice ::", sentence);
// If the practice definition has an `init`, run it.
// Note that we'll need to ground any of the practice's role variables
// that are used within an `init` outcome to perform that outcome.
if (practiceDef.init && practiceDef.init.length > 0) {
const roleBindings = {};
for (let i = 0; i < practiceDef.roles.length; i++) {
const roleName = practiceDef.roles[i];
const roleValue = sentenceParts[i + 2];
if (!roleValue) console.warn("Missing role value", practiceDef.id, roleName);
roleBindings[roleName] = roleValue;
}
for (const initOutcome of practiceDef.init) {
const groundedOutcome = groundOutcome(initOutcome, roleBindings);
performOutcome(praxishState, groundedOutcome);
}
}
}
}
else if (op === "delete") {
const sentence = parts[1];
retract(praxishState.db, sentence);
}
else if (op === "call") {
// Look up the function to execute.
// FIXME The current lookup process has to search within each practice definition
// for a function with the specified name, which is unnecessarily slow.
// We can improve performance by building a function registry at `definePractice` time.
const functionName = parts[1];
let functionDef = null;
for (const practiceDef of Object.values(praxishState.practiceDefs)) {
const functionDefs = practiceDef.functions || [];
functionDef = functionDefs.find(fdef => fdef.name === functionName);
if (functionDef) break;
}
if (!functionDef) {
console.warn("Couldn't find function", functionName);
}
// Establish bindings for the function's parameters, if any.
const paramNames = functionDef.params || [];
const paramVals = parts.slice(2);
const paramBindings = {};
for (let i = 0; i < paramNames.length; i++) {
paramBindings[paramNames[i]] = paramVals[i];
}
// Determine which of the function's cases to execute (if any) and execute it.
for (const caseDef of functionDef.cases) {
const results = query(praxishState.db, caseDef.conditions, paramBindings);
if (results.length > 0) {
// Execute this case with the first available bindings and stop trying other cases.
// FIXME Only one case should be executed per `call`, right?
const result = results[0];
for (const outcomeDef of caseDef.outcomes || []) {
const outcome = groundOutcome(outcomeDef, result);
performOutcome(praxishState, outcome); // FIXME Possible infinite recursion if `call`
}
break;
}
}
}
else {
console.warn("Bad outcome op", op, outcome);
}
return praxishState;
}
// Given a `praxishState` and an `action` (i.e., a map of bindings including
// at least `Actor`, `practiceID`, `instanceID`, and `actionID`),
// perform the `action` and return the updated `praxishState`.
function performAction(praxishState, action) {
const practiceDef = praxishState.practiceDefs[action.practiceID];
const actionDef = practiceDef.actions.find(adef => adef.name === action.actionID);
for (const outcomeDef of actionDef.outcomes || []) {
const outcome = groundOutcome(outcomeDef, action);
performOutcome(praxishState, outcome);
}
return praxishState;
}
// Given a `praxishState`, determine whose turn it is to act,
// select an action for that character to perform, and perform the action.
function tick(praxishState) {
// Figure out whose turn it is to act. For now, turntaking will just be simple round-robin.
praxishState.actorIdx += 1;
if (praxishState.actorIdx > praxishState.allChars.length - 1) praxishState.actorIdx = 0;
const actor = praxishState.allChars[praxishState.actorIdx];
// Get all possible actions for the current actor.
const possibleActions = getAllPossibleActions(praxishState, actor.name);
// Figure out what action to perform.
// Practice-bound actors should perform random available actions from their practice;
// actors with goals should select actions that seem to advance their goals;
// actors without goals can do whatever.
let actionToPerform = null;
if (actor.boundToPractice) {
// Filter possible actions to just those from the bound practice.
// FIXME We should probably move this logic into `getAllPossibleActions`
// so that we don't waste time generating actions that will never be performed.
const practiceActions = possibleActions.filter(pa => pa.practiceID === actor.boundToPractice);
actionToPerform = randNth(practiceActions);
}
else if (actor.goals && possibleActions.length > 0) {
// Speculatively perform each possible action
// and score the outcome according to the actor's goals.
for (const possibleAction of possibleActions) {
const prevDB = clone(praxishState.db);
performAction(praxishState, possibleAction);
possibleAction.score = 0;
for (const goal of actor.goals) {
const results = query(praxishState.db, goal.conditions, {});
possibleAction.score += (goal.utility * results.length);
}
praxishState.db = prevDB;
}
// Select an action for the actor to perform,
// randomly choosing among top-scoring actions for this actor's goals.
possibleActions.sort((a, b) => b.score - a.score);
const topScore = possibleActions[0].score;
const firstNonTopscoringIdx = possibleActions.findIndex(pa => pa.score < topScore);
if (firstNonTopscoringIdx > -1) {
const bestScoringActions = possibleActions.slice(0, firstNonTopscoringIdx);
actionToPerform = randNth(bestScoringActions);
}
else {
actionToPerform = randNth(possibleActions);
}
}
else {
// Select a random action to perform.
actionToPerform = randNth(possibleActions);
}
// Perform the action, if any.
if (!actionToPerform) {
console.warn("No actions to perform", actor.name);
return;
}
console.log("Performing action ::", actionToPerform.name, actionToPerform);
performAction(praxishState, actionToPerform);
}