UNPKG

41 kBJavaScriptView Raw
1var fs = require('fs');
2var PEG = require("pegjs");
3var util = require('util');
4var combinatorics = require('./js-combinatorics/combinatorics.js').Combinatorics;
5
6/*
7AI Planning with STRIPS and PDDL.
8
9Copyright (c) 2017 Kory Becker
10http://primaryobjects.com/kory-becker
11
12License MIT
13*/
14
15StripsManager = {
16 // Set to false to use baseN() instead of permutationCombination() for parameter values. It will be slower, but will utilize all possible solutions. This allows rendering of 'complementary actions', such as 'Action A on A', where normally you want 'Action A on B'.
17 fast: true,
18 // Set to true to display status information on the console while searching for a solution.
19 verbose: false,
20 // Set to redirect output to different stream, uses console.log() by default.
21 output: function(text) { console.log(text); },
22 // PEG.js grammar for domain.
23 grammarDomainPath: __dirname + '/grammar/grammar-domain.txt',
24 // PEG.js grammer for problem.
25 grammarProblemPath: __dirname + '/grammar/grammar-problem.txt',
26
27 loadCode: function(grammarFileName, code, callback) {
28 // Applies a PEG.js grammar against a code string and returns the parsed JSON result.
29 fs.readFile(grammarFileName, 'utf8', function(err, grammar) {
30 if (err) throw err;
31
32 var parser = PEG.generate(grammar);
33
34 if (callback) {
35 callback(parser.parse(code));
36 }
37 });
38 },
39
40 loadGrammar: function(grammarFileName, codeFileName, callback) {
41 // Applies a PEG.js grammar against a code file and returns the parsed JSON result.
42 fs.readFile(codeFileName, 'utf8', function(err, code) {
43 if (err) throw err;
44
45 StripsManager.loadCode(grammarFileName, code, function(result) {
46 if (callback) {
47 callback(result);
48 }
49 });
50 });
51 },
52
53 loadDomain: function(filePath, callback, isCode) {
54 // Applies the PEG.js grammar for a STRIPS PDDL domain file and returns the parsed JSON result.
55 if (!isCode) {
56 StripsManager.loadGrammar(StripsManager.grammarDomainPath, filePath, function(result) {
57 // Load from file path.
58 if (callback) {
59 callback(result);
60 }
61 });
62 }
63 else {
64 // Load from string.
65 StripsManager.loadCode(StripsManager.grammarDomainPath, filePath, function(result) {
66 if (callback) {
67 callback(result);
68 }
69 });
70 }
71 },
72
73 loadProblem: function(filePath, callback, isCode) {
74 // Applies the PEG.js grammar for a STRIPS PDDL problem file and returns the parsed JSON result.
75 if (!isCode) {
76 // Load from file path.
77 StripsManager.loadGrammar(StripsManager.grammarProblemPath, filePath, function(problem) {
78 StripsManager.initializeProblem(problem, callback)
79 });
80 }
81 else {
82 // Load from string.
83 StripsManager.loadCode(StripsManager.grammarProblemPath, filePath, function(problem) {
84 StripsManager.initializeProblem(problem, callback)
85 });
86 }
87 },
88
89 initializeProblem: function(problem, callback) {
90 // Populate list of parameter values.
91 var values = {};
92 for (var i in problem.states) {
93 var state = problem.states[i];
94 for (var j in state.actions) {
95 var action = state.actions[j];
96
97 // Collect all unique parameter values.
98 for (var k in action.parameters) {
99 values[action.parameters[k]] = 1;
100 }
101 }
102 }
103
104 // Set parameter values list on problem.
105 problem.values = {};
106 for (var key in values) {
107 // Look-up type for this value in the objects declaration.
108 var type = null;
109
110 for (var i in problem.objects) {
111 for (var j in problem.objects[i].parameters) {
112 var parameter = problem.objects[i].parameters[j];
113 if (parameter == key) {
114 type = problem.objects[i].type;
115 break;
116 }
117 }
118
119 if (type)
120 break;
121 }
122
123 problem.values[type] = problem.values[type] || [];
124 problem.values[type].push(key);
125 }
126
127 if (callback) {
128 callback(problem);
129 }
130 },
131
132 load: function(domainPath, problemPath, callback, isCode) {
133 // Load the domain and actions. If isCode is true, domainPath and problemPath are strings of PDDL code, otherwise they are filePaths.
134 StripsManager.loadDomain(domainPath, function(domain) {
135 // Load the problem.
136 StripsManager.loadProblem(problemPath, function(problem) {
137 // Give a copy of the possible parameter values to the domain.
138 domain.values = problem.values;
139
140 if (domain.requirements.indexOf('typing') != -1 && domain.values.null) {
141 StripsManager.output('ERROR: :typing is specified in domain, but not all parameters declare a type. Verify problem file contains an :objects section.');
142 }
143
144 // Load list of applicable combinations of parameter values for each action.
145 for (var i in domain.actions) {
146 // Get all applicable parameter combinations for the current action.
147 domain.actions[i].parameterCombinations = StripsManager.parameterCombinations(domain, domain.actions[i]);
148 }
149
150 if (callback) {
151 callback(domain, problem);
152 }
153 }, isCode);
154 }, isCode);
155 },
156
157 predicateCombinations: function(state) {
158 // For "Blocks World" problems, combinatorics.permutationCombination(state) is sufficient and faster, but otherwise, baseN(state) gives the full range of possible parameter values.
159 // First, convert the values object { block: [ 'a', 'b'], table: ['x', 'y'] } into a flat array [ 'a', 'b', 'x', 'y' ].
160 var values = [];
161 for (var key in state) {
162 for (var i in state[key]) {
163 values.push(state[key][i]);
164 }
165 }
166
167 var cmb = StripsManager.fast ? combinatorics.permutationCombination(values) : combinatorics.baseN(values);
168
169 return cmb.toArray();
170 },
171
172 parameterCombinations: function(domain, action) {
173 // Go through each required parameter, look at the type (if using :typing), and use all combinations of values belonging to that type.
174 var cases = [];
175 var parameters = action.parameters;
176
177 // Is :typing enabled on the domain?
178 if (domain.requirements.indexOf('typing') > -1) {
179 // First, get a count of how many parameters we need of each type.
180 var error = false;
181 var typeCounts = {};
182 for (var j in parameters) {
183 if (!parameters[j].type) {
184 StripsManager.output('ERROR: :typing is specified, but no type found in action "' + action.action + '" for parameter "' + parameters[j].parameter + '"');
185 error = true;
186 break;
187 }
188
189 typeCounts[parameters[j].type] = (typeCounts[parameters[j].type] + 1) || 1;
190 }
191
192 if (!error) {
193 // Next, get the combination values.
194 for (var key in typeCounts) {
195 // Get all combination values for this parameter type.
196 var values = domain.values[key];
197 if (values) {
198 var cmb = combinatorics.baseN(values, 1);
199
200 cmb.forEach(function(combo) {
201 cases.push(combo);
202 });
203 }
204 }
205 }
206
207 // Get a combination of all possibilities of the discovered parameters.
208 var cmb = StripsManager.fast ? combinatorics.permutation(cases, parameters.length) : combinatorics.baseN(cases, parameters.length);
209
210 // Filter the combinations to valid parameter types and unique combos.
211 var uniqueCombos = {};
212 cases = cmb.filter(function (combo) {
213 // Does this combo have valid values for the type? Make sure each value to be set for a parameter index exists in the list of types under the domain.
214 var key = '';
215
216 for (var ci in combo) {
217 var value = combo[ci][0];
218 var type = parameters[ci].type;
219 key += value;
220
221 // Check if this value exists in the list for this type.
222 if (!domain.values[type] || (domain.values[type] && domain.values[type].indexOf(value) == -1)) {
223 // The value is not part of this type, that means this combo is invalid.
224 return false;
225 }
226 }
227
228 if (uniqueCombos[key]) {
229 // Duplicate combo. Since we only take the first value in any lists as 1 value per parameter, we can end up with duplicates.
230 return false;
231 }
232
233 uniqueCombos[key] = 1;
234
235 return true;
236 });
237
238 var cases2 = [];
239 for (var j in cases) {
240 var subCase = [];
241 for (var k in cases[j]) {
242 subCase.push(cases[j][k][0]);
243 }
244
245 cases2.push(subCase);
246 }
247
248 cases = cases2;
249 }
250 else {
251 // Typing not being used, just get all action combinations for the current state.
252 cases = StripsManager.predicateCombinations(domain.values);
253 }
254
255 return cases;
256 },
257
258 andCount: function(precondition) {
259 // Returns the count for the number of 'and' matches in a precondition.
260 var count = 0;
261
262 for (var i in precondition) {
263 var action = precondition[i];
264 var operation = action.operation || 'and'; // If no operation is specified, default to 'and'. Must explicitly provide 'not' where required.
265
266 if (operation == 'and') {
267 count++;
268 }
269 }
270
271 return count;
272 },
273
274 isEqual: function(action1, action2) {
275 // Returns true if action1 == action2. Compares name and parameters.
276 var result = false;
277
278 // Find matching action name.
279 if (action1.action == action2.action && action1.parameters.length == action2.parameters.length) {
280 result = true;
281
282 // Find matching parameters.
283 for (var k in action1.parameters) {
284 // Use the map, if available (in the case of a non-concrete action). Otherwise, use the concrete parameter values.
285 var value1 = action1.parameters[k].parameter ? action1.parameters[k].parameter : action1.parameters[k];
286 var value2 = action2.parameters[k].parameter ? action2.parameters[k].parameter : action2.parameters[k];
287
288 var parameter1 = action1.map ? action1.map[value1] : value1;
289 var parameter2 = action2.map ? action2.map[value2] : value2;
290
291 if (parameter1 != parameter2) {
292 result = false;
293 break;
294 }
295 }
296 }
297
298 return result;
299 },
300
301 isPreconditionSatisfied: function(state, precondition) {
302 // Returns true if the precondition is satisfied in the current state.
303 // This function works by making sure all 'and' preconditions exist in the state, and that all 'not' preconditions do not exist in the state.
304 var matchCount = 0;
305 var andCount = StripsManager.andCount(precondition); // The state needs to contain the actions in action.precondition for 'and'. For 'not', we fail immediately. So, let's count the number of 'and' matches and make sure we satisfy them.
306
307 for (var i = 0; i < precondition.length; i++) {
308 // Find a case that contains this action and parameters.
309 for (var l in state.actions) {
310 var match = true;
311 operation = precondition[i].operation || 'and'; // If no operation is specified, default to 'and'. Must explicitly provide 'not' where required.
312
313 // Check if the name and number of parameters match for the current action and precondition.
314 if (state.actions[l].action == precondition[i].action && state.actions[l].parameters.length == precondition[i].parameters.length) {
315 // Check if the parameter values match.
316 for (var m in precondition[i].parameters) {
317 if (precondition[i].parameters[m] != state.actions[l].parameters[m]) {
318 match = false;
319 }
320 }
321 }
322 else {
323 match = false;
324 }
325
326 if (match) {
327 // This action exists in the state.
328 if (operation == 'and') {
329 matchCount++;
330 }
331 else {
332 // Not, set to -1 so this action is not saved as applicable.
333 matchCount = -1;
334 break;
335 }
336 }
337 }
338
339 if (matchCount == -1)
340 break;
341 }
342
343 return (matchCount == andCount);
344 },
345
346 getApplicableActionInState: function(state, action) {
347 // This function returns an applicable concrete action for the given state, or null if the precondition is not satisfied.
348 var resolvedAction = null;
349
350 // Does the filled-in precondition exist in the state test cases?
351 if (StripsManager.isPreconditionSatisfied(state, action.precondition)) {
352 // This action is applicable.
353 // Assign a value to each parameter of the effect.
354 var populatedEffect = JSON.parse(JSON.stringify(action.effect));
355 for (var m in action.effect) {
356 var effect = action.effect[m];
357
358 for (var n in effect.parameters) {
359 var parameter = effect.parameters[n];
360 var value = action.map[parameter];
361
362 if (value) {
363 // Assign this value to all instances of this parameter in the effect.
364 populatedEffect[m].parameters[n] = value;
365 }
366 else {
367 StripsManager.output('* ERROR: Value not found for parameter ' + parameter + '.');
368 }
369 }
370 }
371
372 resolvedAction = JSON.parse(JSON.stringify(action));
373 resolvedAction.effect = populatedEffect;
374 resolvedAction.map = action.map;
375 }
376
377 return resolvedAction;
378 },
379
380 applicableActionsPlus: function(domain, state) {
381 // Returns an array of applicable concrete actions for the current state, including support for negative literals. This method runs StripsManager.applicableActions() two times - one with all positive literals (negative literals removed, which effectively renders all positive literal cases), and one with all positive literals with none that had matching negative literals (which effectively renders all negative literal cases). The result includes a list of unique actions.
382 var result = [];
383 var actionHash = {};
384
385 // Remove negative literals.
386 var stateNoNegatives = JSON.parse(JSON.stringify(state));
387 stateNoNegatives.actions = [];
388 for (var i in state.actions) {
389 var action = state.actions[i];
390
391 if (action.operation != 'not') {
392 // Not a negative literal, so keep it.
393 stateNoNegatives.actions.push(action);
394 }
395 }
396
397 // Get applicable actions.
398 var actions = StripsManager.applicableActions(domain, stateNoNegatives);
399
400 // Mark each action as discovered.
401 for (var i in actions) {
402 var action = actions[i];
403
404 result.push(action);
405 actionHash[JSON.stringify(action)] = 1;
406 }
407
408 // Remove matching positive and negative literals, effectively rendering the negative literal.
409 var literalsToRemove = {};
410 var stateNoPositiveNegatives = JSON.parse(JSON.stringify(state));
411 stateNoPositiveNegatives.actions = [];
412
413 // First, collect negative literals.
414 for (var i in state.actions) {
415 var action = state.actions[i];
416 action.operation = action.operation || 'and';
417
418 if (action.operation == 'not') {
419 // Make a copy of the positive version of this literal.
420 var copyAction = JSON.parse(JSON.stringify(action));
421 copyAction.operation = 'and';
422
423 // Mark the positive version of this literal to be removed (if we come across it).
424 literalsToRemove[JSON.stringify(copyAction)] = 1;
425 }
426 }
427
428 // Now that we've marked negative literals, go through all literals and only keep those which are positive and not included in the literalsToRemove.
429 for (var i in state.actions) {
430 var action = state.actions[i];
431 action.operation = action.operation || 'and';
432
433 // If this is a positive literal and not in our literalsToRemove list, then include it.
434 if (action.operation != 'not' && !literalsToRemove[JSON.stringify(action)]) {
435 // Safe to keep this literal.
436 stateNoPositiveNegatives.actions.push(action);
437 }
438 }
439
440 // Get applicable actions when allowing for negative literals.
441 actions = StripsManager.applicableActions(domain, stateNoPositiveNegatives);
442
443 // Concat new actions.
444 for (var i in actions) {
445 var action = actions[i];
446
447 if (!actionHash[JSON.stringify(action)]) {
448 result.push(action);
449 }
450 }
451
452 return result;
453 },
454
455 applicableActions: function(domain, state) {
456 // Returns an array of applicable concrete actions for the current state, using the possible parameter values in domain.values array (Example: values = ['a', 'b', 't1', 't2', 't3']).
457 // Test each domain action precondition against the cases. If one holds valid, then that action is applicable in the current state.
458 var result = [];
459
460 if (!domain.values || domain.values.length == 0) {
461 StripsManager.output('ERROR: No parameter values found in domain.values.');
462 return;
463 }
464
465 for (var i in domain.actions) {
466 var action = domain.actions[i]; // op1
467 var parameters = action.parameters; // x1, x2, x3
468 var populatedAction = JSON.parse(JSON.stringify(action)); // copy for replacing parameters with actual values.
469 var parameterMapHash = {};
470
471 // Assign values to the parameters for each test case.
472 for (var j in action.parameterCombinations) {
473 var testCase = action.parameterCombinations[j];
474 var nindex = 0;
475
476 var parameterMap = []; // map of parameter values to be populated
477 // Initialize default parameter values for this action. We'll set concrete values next.
478 for (var j in parameters) {
479 parameterMap[parameters[j].parameter] = testCase[nindex++];
480 }
481
482 // Get the action's precondition parameters.
483 var testCaseIndex = 0;
484 for (var k in action.precondition) {
485 var precondition = action.precondition[k];
486 var populatedPreconditionPart = JSON.parse(JSON.stringify(precondition)); // copy for replacing parameters with actual values.
487
488 // Found a matching action. So far, so good.
489 var parameterIndex = 0;
490
491 // Assign a value to each parameter of the precondition.
492 for (var l in precondition.parameters) {
493 var parameter = precondition.parameters[l];
494 var value = parameterMap[parameter];
495
496 // Assign this value to all instances of this parameter in the precondition.
497 populatedPreconditionPart.parameters[l] = value;
498 }
499
500 populatedAction.precondition[k] = populatedPreconditionPart;
501 populatedAction.map = parameterMap;
502 }
503
504 // Does the filled-in precondition exist in the test cases?
505 var applicableAction = StripsManager.getApplicableActionInState(state, populatedAction);
506 if (applicableAction) {
507 // This action is applicable in this state. Make sure we haven't already found this one.
508 var isDuplicate = false;
509 for (var rr in result) {
510 var action1 = result[rr];
511 if (StripsManager.isEqual(applicableAction, action1)) {
512 isDuplicate = true;
513 break;
514 }
515 }
516
517 if (!isDuplicate) {
518 result.push(applicableAction);
519 }
520 }
521 }
522 }
523
524 return result;
525 },
526
527 applyAction: function(action, state) {
528 // Applies an action on a state and returns the new state. It is assumed that the precondition has already been tested.
529 var result = JSON.parse(JSON.stringify(state));
530
531 for (var i in action.effect) {
532 var actionOperation = action.effect[i];
533 var operation = actionOperation.operation || 'and';
534
535 if (operation == 'and') {
536 // Make sure this predicate doesn't already exist in the state.
537 var isExists = false;
538 for (var j in state.actions) {
539 // Find matching action.
540 if (StripsManager.isEqual(state.actions[j], actionOperation)) {
541 isExists = true;
542 break;
543 }
544 }
545
546 if (!isExists) {
547 // Add this predicate to the state.
548 result.actions.push(actionOperation);
549 }
550 }
551 else {
552 // Remove this predicate from the state.
553 for (var j in state.actions) {
554 // Find matching action.
555 if (StripsManager.isEqual(state.actions[j], actionOperation)) {
556 // This is our target. Find the same item in our result list (since result may now have different indices than state.actions, if new actions were added via 'and').
557 for (var k in result.actions) {
558 if (StripsManager.isEqual(state.actions[j], result.actions[k])) {
559 result.actions.splice(k, 1);
560 }
561 }
562 }
563 }
564 }
565 }
566
567 return result;
568 },
569
570 getChildStates: function(domain, state) {
571 // Returns the list of child states for the current state, after applying all applicable actions.
572 var children = [];
573
574 var actions = StripsManager.applicableActions(domain, state);
575 for (var i in actions) {
576 var action = actions[i];
577 children.push({ state: StripsManager.applyAction(action, state), action: action });
578 }
579
580 return children;
581 },
582
583 isGoal: function(state, goalState) {
584 // Returns true if the state contains the goal conditions.
585 var result = true;
586
587 for (var i in goalState.actions) {
588 var goalAction = goalState.actions[i];
589 var operation = goalAction.operation || 'and';
590
591 if (operation == 'and') {
592 // Make sure this action exists in the state.
593 var isExists = false;
594 for (var j in state.actions) {
595 if (StripsManager.isEqual(state.actions[j], goalAction)) {
596 isExists = true;
597 break;
598 }
599 }
600
601 // If we found a match, then this goal action exists. Move on to next tests.
602 if (!isExists) {
603 result = false;
604 break;
605 }
606 }
607 else {
608 // Make sure this action does not exist in the state.
609 var isExists = false;
610 for (var j in state.actions) {
611 if (StripsManager.isEqual(state.actions[j], goalAction)) {
612 // This is our target, so it fails the goal test.
613 isExists = true;
614 break;
615 }
616 }
617
618 if (isExists) {
619 // Found a match for 'not', so goal fails.
620 result = false;
621 break;
622 }
623 }
624 }
625
626 return result;
627 },
628
629 actionToString: function(action) {
630 var result = action.action;
631
632 for (var key in action.map) {
633 result += ' ' + action.map[key];
634 }
635
636 return result;
637 },
638
639 stateToString: function(state) {
640 var result = '';
641 var actionList = [];
642
643 for (var i in state.actions) {
644 var action = state.actions[i];
645
646 var actionString = '(' + action.action;
647 for (var j in action.parameters) {
648 actionString += ' ' + action.parameters[j];
649 }
650 actionString += ')';
651
652 // Keep a list of actions so we can sort them. This allows two states with different orderings of the same actions to result in the same string.
653 actionList.push(actionString);
654 }
655
656 for (var i in actionList.sort()) {
657 if (i > 0) {
658 result += ' ';
659 }
660 result += actionList[i];
661 }
662
663 return result;
664 },
665
666 solve: function(domain, problem, isDfs, maxSolutions, cost) {
667 // Find solution using A*, depth-first, or breadth-first search.
668 if (typeof(isDfs) == 'function' && !cost) {
669 // Allow passing cost as 3rd parameter.
670 cost = isDfs;
671 }
672 else if (isDfs == null) {
673 // If no other option specified, use depth-first-search by default.
674 isDfs = true;
675 }
676
677 maxSolutions = maxSolutions || 1;
678
679 if (cost && typeof(cost) != 'function') {
680 StripsManager.output('ERROR: parameter "cost" must be a function to serve as the A* algorithm heuristic. Method: solve(domain, problem, isDepthFirstSearch, cost, maxSolutions). Usage: solve(domain, problem), solve(domain, problem, false), solve(domain, problem, cost).');
681 return;
682 }
683
684 if (StripsManager.verbose) {
685 StripsManager.output('Using ' + (cost ? 'A*' : (isDfs ? 'depth' : 'breadth') + '-first-search') + '.');
686 StripsManager.output('');
687 }
688
689 return cost ? StripsManager.solveAs(domain, problem.states[0], problem.states[1], cost) :
690 (isDfs ? StripsManager.solveDfs(domain, problem.states[0], problem.states[1], maxSolutions) :
691 StripsManager.solveBfs(domain, problem.states[0], problem.states[1], maxSolutions));
692 },
693
694 solveDfs: function(domain, state, goalState, maxSolutions, visited, depth) {
695 // Find all solutions using depth-first-search.
696 var solutions = [];
697
698 visited = visited ? JSON.parse(JSON.stringify(visited)) : {};
699 depth = depth || 0;
700 state = state.state ? state : { state: state }; // format state to mirror child, which includes parent and action in recursion.
701
702 // If this is the initial state, add it to the visited list.
703 if (Object.keys(visited).length == 0) {
704 visited[StripsManager.stateToString(state.state)] = 1;
705 }
706
707 // Check for goal.
708 if (StripsManager.isGoal(state.state, goalState)) {
709 // Compile solution path.
710 var path = [];
711 var steps = depth;
712
713 while (state != null && state.parent != null) {
714 // Since we move from goal backwards, add this step to the front of the array (rather than the end, otherwise it would be in reverse order).
715 path.unshift(StripsManager.actionToString(state.action));
716 state = state.parent;
717 }
718
719 return [ { steps: steps, path: path } ];
720 }
721 else {
722 // Get child states by applying actions to current state.
723 var fringe = StripsManager.getChildStates(domain, state.state);
724
725 if (StripsManager.verbose) {
726 StripsManager.output('Depth: ' + depth + ', ' + fringe.length + ' child states.');
727 }
728
729 // Run against each new child state.
730 for (var i in fringe) {
731 var child = fringe[i];
732 child.parent = state;
733 var key = StripsManager.stateToString(child.state);
734
735 if (!visited[key]) {
736 visited[key] = 1;
737 var subSolutions = StripsManager.solveDfs(domain, child, goalState, maxSolutions, visited, depth + 1);
738 if (subSolutions.length > 0) {
739 // This branch has a solution(s).
740 for (var j in subSolutions) {
741 solutions.push(subSolutions[j]);
742
743 if (solutions.length >= maxSolutions) {
744 break;
745 }
746 }
747
748 if (solutions.length >= maxSolutions) {
749 break;
750 }
751 }
752 }
753 }
754 }
755
756 return solutions;
757 },
758
759 solveBfs: function(domain, state, goalState, maxSolutions) {
760 // Find all solutions using breadth-first-search.
761 var fringe = [ { state: state, depth: 0 } ]; // Start with the initial state on the fringe.
762 var visited = {};
763 var depth = 0;
764 var solutions = [];
765
766 while (fringe.length > 0) {
767 // Investigate the next state with the lowest depth.
768 var current = fringe[0];
769
770 // Remove this state from the fringe.
771 fringe.shift();
772
773 // Mark this state as visited.
774 visited[StripsManager.stateToString(current.state)] = 1;
775
776 // Check for goal.
777 if (StripsManager.isGoal(current.state, goalState)) {
778 // Compile solution path.
779 var path = [];
780 var steps = current.depth;
781
782 while (current != null && current.parent != null) {
783 // Since we move from goal backwards, add this step to the front of the array (rather than the end, otherwise it would be in reverse order).
784 path.unshift(StripsManager.actionToString(current.action));
785 current = current.parent;
786 }
787
788 solutions.push({ steps: steps, path: path });
789
790 if (solutions.length >= maxSolutions) {
791 return solutions;
792 }
793 }
794 else {
795 // Get child states by applying actions to current state.
796 var children = StripsManager.getChildStates(domain, current.state);
797
798 // Add the children to the fringe.
799 for (var i in children) {
800 var child = children[i];
801 child.parent = current;
802 child.depth = current.depth + 1;
803
804 if (!visited[StripsManager.stateToString(child.state)]) {
805 fringe.push(child);
806 }
807 }
808 }
809
810 if (StripsManager.verbose) {
811 StripsManager.output('Depth: ' + current.depth + ', ' + fringe.length + ' child states.');
812 }
813 }
814
815 return solutions;
816 },
817
818 solveAs:function(domain, state, goalState, cost) {
819 // Find first solution using A* search, where cost is the heuristic function (h = cost(state)). Starting with the initial state, we find all children by applying applicable actions on the current state, calculate the child state costs, and select the next cheapest state to visit.
820 var depth = 0;
821 var fringe = [ { state: state, h: cost(state), g: depth } ]; // Start with the initial state on the fringe.
822 var visited = {};
823 var solutions = [];
824
825 while (fringe.length > 0) {
826 // Investigate the next state with the lowest cost.
827 var current = fringe[0];
828
829 // Remove this state from the fringe.
830 fringe.shift();
831
832 // Mark this state as visited.
833 visited[StripsManager.stateToString(current.state)] = 1;
834
835 // Check for goal.
836 if (StripsManager.isGoal(current.state, goalState)) {
837 // Compile solution path.
838 var path = [];
839 var steps = current.g;
840
841 while (current != null && current.parent != null) {
842 // Since we move from goal backwards, add this step to the front of the array (rather than the end, otherwise it would be in reverse order).
843 path.unshift(StripsManager.actionToString(current.action));
844 current = current.parent;
845 }
846
847 solutions.push({ steps: steps, path: path });
848
849 return solutions;
850 }
851 else {
852 // Get child states by applying actions to current state.
853 var children = StripsManager.getChildStates(domain, current.state);
854
855 // Add the children to the fringe.
856 for (var i in children) {
857 var child = children[i];
858 child.parent = current;
859 child.g = current.g + 1;
860 child.h = cost(child.state);
861
862 if (!visited[StripsManager.stateToString(child.state)]) {
863 fringe.push(child);
864 }
865 }
866
867 fringe.sort(function(a, b) { return (a.h + a.g) - (b.h + b.g) });
868 }
869
870 if (StripsManager.verbose) {
871 StripsManager.output('Depth: ' + current.g + ', Current cost: ' + (current.h + current.g) + ', ' + fringe.length + ' child states.');
872 }
873 }
874
875 return solutions;
876 },
877
878 nextGraphLayer: function(domain, parentLayer, isSkipNegativeLiterals) {
879 // Builds the next planning graph layer, based upon the previous layer. In each action, 'precondition' represents parent literals. 'effect' represents child literals.
880 // Returns a 3-tier layer, consisting of P0 (literals), A0 (actions), P1 (literals). The format is: P0 = precondition, A0 = all actions not named 'noop', P1 = effect.
881 // If isSkipNegativeLiterals = true, negative literals (mutex) created from an action will be ignored.
882 var layer = [];
883 var literalHash = {};
884 var literalCount = 0;
885 var actionCount = 0;
886
887 // Pack all literals from actions in this layer into a single array.
888 var children = { effect: [] };
889 for (var i in parentLayer) {
890 for (var j in parentLayer[i].effect) {
891 var literal = parentLayer[i].effect[j];
892 literal.operation = literal.operation || 'and';
893
894 if (!isSkipNegativeLiterals || (isSkipNegativeLiterals && literal.operation != 'not')) {
895 if (!literalHash[JSON.stringify(literal)]) {
896 children.effect.push(literal);
897
898 // P2 - Carry forward literals from parent, using noop actions.
899 var noop = { action: 'noop' };
900 noop.precondition = noop.precondition || [];
901 noop.precondition.push(literal);
902 noop.effect = noop.precondition;
903 layer.push(noop);
904
905 literalHash[JSON.stringify(literal)] = 1;
906
907 // Keep a count of all literals in this layer so we know if we found any new ones after graphing.
908 literalCount++;
909 }
910 }
911 }
912 }
913
914 // A1 - Get all applicable actions for the state.
915 var actions = StripsManager.applicableActionsPlus(domain, { actions: children.effect });
916 actionCount = actions.length;
917 for (var i in actions) {
918 // Add action to the layer, preconditions are the parents, effects are the children.
919 layer.push(actions[i]);
920 }
921
922 if (StripsManager.verbose) {
923 StripsManager.output('P' + lastGraphIndex + ': ' + lastLiteralCount + ', A' + (lastGraphIndex+1) + ': ' + lastActionCount + ', P' + (lastGraphIndex+1) + ': ' + literalCount + ', A' + (lastGraphIndex+2) + ': ' + actionCount);
924 }
925
926 lastGraphIndex++;
927 lastLiteralCount = literalCount;
928
929 // If we discovered new literals or new actions, then return the layer and continue building the graph.
930 if (lastLiteralCount > literalCount || lastActionCount != actionCount) {
931 lastActionCount = actionCount;
932
933 return layer;
934 }
935 else {
936 // No change, no new literals.
937 layer.done = true;
938 return layer;
939 }
940 },
941
942 graph: function(domain, problem, minLayers, maxLayers, isSkipNegativeLiterals) {
943 // Builds a planning graph for a domain and problem. In each action, 'precondition' represents parent literals. 'effect' represents child literals. Any action not named 'noop' represents an applicable action.
944 // Each layer consists of 3-tiers: P0 (literals), A0 (actions), P1 (literals). The format is: P0 = precondition, A0 = actions, P1 = effect.
945 // Loops, building new graph layers, until no new literals and no new actions are discovered.
946 // If isSkipNegativeLiterals = true, negative literals (mutex) created from an action will be ignored.
947 var result = [];
948 var layer = [];
949 var actionHash = {};
950
951 // P0 - initial literals.
952 for (var i in problem.states[0].actions) {
953 // P1 - B. Carry forward literals from parent.
954 var noop = { action: 'noop' };
955 noop.precondition = noop.precondition || [];
956 noop.precondition.push(problem.states[0].actions[i]);
957 noop.effect = noop.precondition;
958 layer.push(noop);
959 }
960
961 // A0 - Get all applicable actions for the initial state.
962 var actions = StripsManager.applicableActionsPlus(domain, problem.states[0]);
963
964 // Initialize global graph helper counters.
965 lastLiteralCount = layer.length;
966 lastActionCount = actions.length;
967 lastGraphIndex = 0;
968
969 layer = layer.concat(actions);
970
971 // Add the literals, actions, next literals to the graph (P0, A0, P1).
972 result.push(layer);
973
974 // Next layer.
975 var index = 0;
976 var layer = StripsManager.nextGraphLayer(domain, result[index++], isSkipNegativeLiterals);
977 while ((!layer.done || (minLayers && index < minLayers)) && (!maxLayers || index < maxLayers)) {
978 if (StripsManager.verbose) {
979 StripsManager.output('Processing layer ' + index);
980 }
981
982 result.push(layer);
983
984 // Get next graph layer.
985 layer = StripsManager.nextGraphLayer(domain, result[index++], isSkipNegativeLiterals);
986 }
987
988 return result;
989 }
990};
991
992module.exports = StripsManager;
\No newline at end of file