UNPKG

aima

Version:

Implementation of Russell & Norvig, "Artificial Intelligence - A Modern Approach".

1,777 lines (1,586 loc) 53.9 kB
// Generated by CoffeeScript 2.6.1 // # aima.js // [![NPM Package](https://img.shields.io/npm/v/aima.svg)](https://www.npmjs.com/package/aima) // [![Tests](https://github.com/davidpomerenke/aima.js/workflows/Node%20CI/badge.svg)](https://github.com/davidpomerenke/aima.js/actions?query=workflow%3A%22Node+CI%22) // [![Coverage](https://codecov.io/gh/davidpomerenke/aima.js/branch/master/graph/badge.svg)](https://codecov.io/gh/davidpomerenke/aima.js) // [*Artificial Intelligence - A Modern Approach*](http://aima.cs.berkeley.edu/) (*AIMA*) by Stuart Russell and Peter Norvig is the reference textbook on artificial intelligence. // This package implements some of the algorithms and data structures from the *AIMA* book in function-oriented [CoffeeScript](https://coffeescript.org/), which is compatible with JavaScript. // The focus is on code understandability. // ## Installation and Usage // For using this package as a module in your own Node JavaScript project, install it with the [node package manager](https://github.com/npm/cli): // `npm install aima` // ```javascript // import { Problem, makeEightPuzzle, aStarSearch } from 'aima' // const simpleEightPuzzle = makeEightPuzzle([ // [1, 2, 7], // [6, 0, 4], // [8, 3, 5] // ]) // console.log(Problem.solutionPath(aStarSearch(simpleEightPuzzle))) // ``` // Put the above example code in `example.mjs` and run it: // `node example.mjs` // ## Extensions // - [aima-checkers](https://github.com/davidpomerenke/aima-checkers): Checkers rulebase. // ## Applications // - [aima-checkers-gui](https://github.com/davidpomerenke/checkers): Graphical checkers browsergame for desktop and mobile. // - [while-quine](https://github.com/davidpomerenke/while-quine): Finding a Quine program for the WHILE language. // ## Related Work // To my knowledge, there are exactly two other JavaScript projects related to *AIMA*: // - [aimacode/aima-javascript](https://github.com/aimacode/aima-javascript) is an implementation maintained by *AIMA* co-author Peter Norvig. // Its aim is to power some beautiful [interactive visualizations](http://aimacode.github.io/aima-javascript/). // It is written in browser JavaScript rather than Node Java- / CoffeeScript. // The *aimacode* organization also features repositories with *AIMA* implementations in other programming languages, notably // [Java](https://github.com/aimacode/aima-java) and // [Python](https://github.com/aimacode/aima-python). // - [ajlopez/NodeAima](https://github.com/ajlopez/NodeAima) is an abandoned implementation of only the *vacuum world* in Node JavaScript. // The existing code from these projects still has to be harnessed for this project! // ## Contributing // Every contribution is very welcome: Modifications of existing code to make it more understandable and beautiful; additional algorithms and data structures from the book; additional problems and games; additional usage examples; additional documentation; anything else you have in mind. Please create an issue or a pull request! // Thank you very much in advance for your contribution :) // ## Development // - `npm test` verifies all the assertions in the code. // - `npm run build` removes all assertion statements from the code, as well as all lines which are ended by `# testing only`. This way, the testing can be kept right next to the code it refers to, but it is excluded from the distributed package. The code is then transpiled to JavaScript into the `index.mjs` file. The `index.mjs` file is the content for the NPM package, and also the basis for coverage reporting. // - `npm run coverage` creates a coverage report. It is automatically run via Github actions on each push, and the resulting report is pushed to [CodeCov](https://codecov.io/gh/davidpomerenke/aima.js). // - `npm run dev` is a convenience shortcut for development. You can put CoffeeScript code into a new file `dev.coffee`, `import * from './index.mjs'` and then run it with `npm run dev`. The temporary file `dev.mjs` [is needed](https://github.com/evanw/node-source-map-support/issues/178) for _source map_ support, a technique which enables the JavaScript debugger to refer to the correct CoffeeScript lines, rather than to the lines in the transpiled JavaScript code. Using a separate file instead of developing inside `README.litcoffee` spares you to re-run all the tests there when you just want to run your new tests. // # Code // ## Dependencies var AND, NOT, OR, algorithm, alphaBetaMaxValue, alphaBetaMinValue, applicableUnaryConstraints, array, binaryOperators, cities, combinations, complexEightPuzzle, complexKnuthConjecture, count, decision, f, factorial, inputs, isAttacked, j, k, len, len1, levels, maxValue, minValue, nSteps, nonUnaryConstraints, nrAttackedQueens, operatorIndex, operators, problem, proposition, recursiveDepthLimitedSearch, recursiveHillClimbingSearch, recursiveIterativeDeepeningSearch, recursiveParse, ref, ref1, reflexVacuumAgent, routeFindingProblem, sampleSizes, sevenMajority, sevenMinority, simpleEightPuzzle, simpleKnuthConjecture, solution, state, syntax, tableVacuumAgent, threeInaRow, touringProblem, travelingSalespersonProblem, trueModel, unaryConstraints, unaryOperators, slice = [].slice; import deepEqual from 'deep-equal'; // For testing subroutines which depend on random numbers, we use a seeded random number generator which can be called by `rand.random()`: // ## Utilities // #### Sum Array.prototype.sum = function (f = function (x) { return x; }) { return this.reduce(function (accumulator, element, index, array) { return accumulator + f(element, index, array); }, 0); }; // ### array = [[100, 1000], [200, 2000], [300, 3000]]; // #### Argmax, Argmin Array.prototype.argmin = function (f) { return this.reduce(function (accumulator, element) { if (f(element) < f(accumulator)) { return element; } else { return accumulator; } }); }; // ### Array.prototype.argmax = function (f) { return this.reduce(function (accumulator, element) { if (f(element) > f(accumulator)) { return element; } else { return accumulator; } }); }; // ### array = [[100, 3000], [200, 1000], [300, 2000]]; // #### Max, Min Array.prototype.min = function () { return this.argmin(function (x) { return x; }); }; // ### Array.prototype.max = function () { return this.argmax(function (x) { return x; }); }; // ### array = [3, 5, 4, 1, 2]; // #### Factorial factorial = function (n) { return function () { var results = []; for (var j = 1; 1 <= n ? j <= n : j >= n; 1 <= n ? j++ : j--) { results.push(j); } return results; }.apply(this).reduce(function (accumulator, current) { return accumulator * current; }); }; // ### // ## Intelligent Agents // #### Table-Driven Agent // Confer section 2.4, p. 47. export var TableDrivenAgent = class TableDrivenAgent { constructor(table = {}) { this.table = table; this.percepts = []; } action(percept) { this.percepts.push(percept); return this.table[this.percepts]; } }; // Example: __Table Vacuum Agent__ tableVacuumAgent = new TableDrivenAgent({ [[['A', 'clean']]]: 'right', [[['A', 'dirty']]]: 'suck', [[['B', 'clean']]]: 'left', [[['B', 'dirty']]]: 'suck', [[['A', 'clean'], ['A', 'clean']]]: 'right', [[['A', 'clean'], ['A', 'dirty']]]: 'suck', [[['A', 'clean'], ['B', 'clean']]]: 'left', [[['A', 'clean'], ['B', 'dirty']]]: 'suck', [[['A', 'dirty'], ['A', 'clean']]]: 'right', [[['A', 'dirty'], ['A', 'dirty']]]: 'suck', [[['A', 'dirty'], ['B', 'clean']]]: 'left', [[['A', 'dirty'], ['B', 'dirty']]]: 'suck', [[['B', 'clean'], ['A', 'clean']]]: 'right', [[['B', 'clean'], ['A', 'dirty']]]: 'suck', [[['B', 'clean'], ['B', 'clean']]]: 'left', [[['B', 'clean'], ['B', 'dirty']]]: 'suck', [[['B', 'clean'], ['A', 'clean']]]: 'right', [[['B', 'clean'], ['A', 'dirty']]]: 'suck', [[['B', 'clean'], ['B', 'clean']]]: 'left', [[['B', 'clean'], ['B', 'dirty']]]: 'suck', [[['A', 'clean'], ['A', 'clean'], ['A', 'clean']]]: 'right', [[['A', 'clean'], ['A', 'clean'], ['A', 'dirty']]]: 'suck' //... }); // ### // assert.equal tableVacuumAgent.action([ ['A', 'dirty'] ]), 'suck' // assert.equal tableVacuumAgent.action([ ['A', 'clean'] ]), 'right' // assert.equal tableVacuumAgent.action([ ['B', 'dirty'] ]), undefined // #### Simple Reflex Agent // Confer section 2.4, p. 49. export var SimpleReflexAgent = class SimpleReflexAgent { constructor(rules) { this.rules = rules; } action(percept) { var rule, state; state = this.interpretInput(percept); rule = this.rules.find(function (rule) { return rule.condition(...state); }); return rule != null ? rule.action : void 0; } interpretInput(percept) { return percept; } }; // Example: __Reflex Vacuum Agent__ reflexVacuumAgent = new SimpleReflexAgent([{ condition: function (arg) { var arg, status; [status] = slice.call(arg, -1); return status === 'dirty'; }, action: 'suck' }, { condition: function (arg) { var arg, location; [location] = arg; return location === 'A'; }, action: 'right' }, { condition: function (arg) { var arg, location; [location] = arg; return location === 'B'; }, action: 'left' }]); // ### // assert.equal (reflexVacuumAgent.action [ ['A', 'dirty'] ]), 'suck' // assert.equal (reflexVacuumAgent.action [ ['A', 'clean'] ]), 'right' // assert.equal (reflexVacuumAgent.action [ ['B', 'dirty'] ]), 'suck' // assert.equal (reflexVacuumAgent.action [ ['C', 'dirty'] ]), 'suck' // assert.equal (reflexVacuumAgent.action [ ['C', 'clean'] ]), undefined // ## Problem Solving // For the node structure, confer section 3.3.1, p. 79. export var Problem = class Problem { constructor({ initialState: initialState1, actions: actions1, result }) { this.initialState = initialState1; this.actions = actions1; this._result = result; } result(state, action) { if (this.actions(state).some(function (a) { return deepEqual(action, a); })) { return this._result(state, action); } } rootNode() { return { state: this.initialState }; } childNode(node, action) { return { state: this.result(node.state, action), parent: node, action: action }; } expand(node) { var action, j, len, ref, results; ref = this.actions(node.state); results = []; for (j = 0, len = ref.length; j < len; j++) { action = ref[j]; results.push(this.childNode(node, action)); } return results; } static solutionPath(node) { if ('parent' in node) { return [...Problem.solutionPath(node.parent), node.state]; } else { return [node.state]; } } }; // ## Search // Confer section 3.1.1, p. 67. export var SearchProblem = class SearchProblem extends Problem { constructor({ initialState, actions, result, stepCost, heuristic = function () { return 0; }, goalTest }) { super({ initialState: initialState, actions: actions, result: result }); this.heuristic = heuristic; this.goalTest = goalTest; this._stepCost = stepCost; } stepCost(state, action) { if (this.actions(state).some(function (a) { return deepEqual(action, a); })) { return this._stepCost(state, action); } } rootNode() { return { ...super.rootNode(), pathCost: 0, heuristic: this.heuristic(this.initialState) }; } childNode(node, action) { return { ...super.childNode(node, action), pathCost: node.pathCost + this.stepCost(node.state, action), heuristic: this.heuristic(this.result(node.state, action)) }; } }; // ### Toy Problems // #### Vacuum World // Confer section 3.2.1, p. 70. export var vacuumWorld = new SearchProblem({ initialState: { location: 'A', A: 'dirty', B: 'dirty' }, actions: function (state) { return ['left', 'right', 'suck']; }, result: function (state, action) { return { location: action === 'left' ? 'A' : action === 'right' ? 'B' : state.location, A: state.location === 'A' && action === 'suck' ? 'clean' : state.A, B: state.location === 'B' && action === 'suck' ? 'clean' : state.B }; }, stepCost: function (state, action) { return 1; }, goalTest: function (state) { return state.A === 'clean' && state.B === 'clean'; } }); // ### state = vacuumWorld.initialState; state = vacuumWorld.result(state, 'suck'); state = vacuumWorld.result(state, 'suck'); state = vacuumWorld.result(state, 'left'); state = vacuumWorld.result(state, 'right'); state = vacuumWorld.result(state, 'suck'); // #### 8-Puzzle // Confer section 3.2.1, p. 71. export var makeEightPuzzle = function (initialState) { var goalPosition, goalState, manhattanDist, moveIsValid, moves, zero; moveIsValid = function (zero) { var ref, ref1; return ((ref = zero.y) === 0 || ref === 1 || ref === 2) && ((ref1 = zero.x) === 0 || ref1 === 1 || ref1 === 2); }; zero = function (state) { // position of zero return { y: state.indexOf(state.filter(function (row) { return row.includes(0); })[0]), x: state.filter(function (row) { return row.includes(0); })[0].indexOf(0) }; }; goalPosition = function (nr) { return [goalState.findIndex(function (row) { return row.includes(nr); }), goalState.find(function (row) { return row.includes(nr); }).indexOf(nr)]; }; manhattanDist = function ([y1, x1], [y2, x2]) { return Math.abs(y1 - y2) + Math.abs(x1 - x2); }; moves = { up: { y: -1, x: 0 }, down: { y: 1, x: 0 }, left: { y: 0, x: -1 }, right: { y: 0, x: 1 } }; goalState = [[0, 1, 2], [3, 4, 5], [6, 7, 8]]; return new SearchProblem({ initialState: initialState, actions: function (state) { return Object.keys(moves).filter(function (key) { return moveIsValid({ y: zero(state).y + moves[key].y, x: zero(state).x + moves[key].x }); }); }, result: function (state, action) { return state.map(function (row, y) { return row.map(function (nr, x) { // Shift zero to new position. if (y === zero(state).y + moves[action].y && x === zero(state).x + moves[action].x) { return 0; // Shift number to old position of zero. } else if (nr === 0) { return state[zero(state).y + moves[action].y][zero(state).x + moves[action].x]; } else { // Keep all other numbers. return nr; } }); }); }, stepCost: function (state, action) { return 1; }, heuristic: function (state) { return state.sum(function (numbers, y) { return numbers.sum(function (number, x) { return manhattanDist([y, x], goalPosition(number)); }); }); }, goalTest: function (state) { return deepEqual(state, goalState); } }); }; // ### simpleEightPuzzle = makeEightPuzzle([[1, 4, 2], [3, 0, 5], [6, 7, 8]]); state = simpleEightPuzzle.initialState; state = simpleEightPuzzle.result(state, 'up'); state = simpleEightPuzzle.result(state, 'left'); // ### complexEightPuzzle = makeEightPuzzle([[7, 2, 4], [5, 0, 6], [8, 3, 1]]); state = complexEightPuzzle.initialState; // #### Incremental 8-Queens Problem // Incremental formulation of the 8-queens problem. Confer section 3.2.1, p. 72. export var incrementalEightQueensProblem = new SearchProblem({ initialState: [], actions: function (state) { var y; y = state.length; if (y < 8) { return [0, 1, 2, 3, 4, 5, 6, 7].filter(function (x) { return !isAttacked([y, x], state); }); } else { return []; } }, result: function (state, action) { return [...state, action]; }, stepCost: function (state, action) { return 0; }, goalTest: function (state) { return state.length === 8; } }); isAttacked = function ([y0, x0], state) { return state.some(function (x, y) { return x === x0 || y === y0 || Math.abs(y - y0) === Math.abs(x - x0); }); }; // ### state = incrementalEightQueensProblem.initialState; state = incrementalEightQueensProblem.result(state, 3); state = incrementalEightQueensProblem.result(state, 5); // #### Knuth Conjecture // Confer section 3.2.1, p. 73. export var makeKnuthConjecture = function (goal) { var calc; calc = function (state) { return state.reduce(function (total, operation) { if (operation.isNumber) { return operation; } else if (operation === 'factorial') { return factorial(total); } else if (operation === 'square_root') { return Math.sqrt(total); } else if (operation === 'floor') { return Math.floor(total); } }); }; return new SearchProblem({ initialState: [4], actions: function (state) { if (Number.isInteger(calc(state))) { return ['square_root', 'factorial']; } else { return ['square_root', 'floor']; } }, result: function (state, action) { return [...state, action]; }, stepCost: function (state, action) { return 1; }, goalTest: function (state) { return calc(state) === goal; } }); }; // ### simpleKnuthConjecture = makeKnuthConjecture(1); complexKnuthConjecture = makeKnuthConjecture(5); state = complexKnuthConjecture.initialState; state = complexKnuthConjecture.result(state, 'factorial'); state = complexKnuthConjecture.result(state, 'factorial'); state = complexKnuthConjecture.result(state, 'square_root'); state = complexKnuthConjecture.result(state, 'square_root'); state = complexKnuthConjecture.result(state, 'square_root'); state = complexKnuthConjecture.result(state, 'square_root'); state = complexKnuthConjecture.result(state, 'square_root'); state = complexKnuthConjecture.result(state, 'floor'); // ### Real World Problems // Confer section 3.1, p. 68. cities = { dist: { Arad: { Zerind: 75, Sibiu: 140, Timisoara: 118 }, Zerind: { Arad: 75, Oradea: 71 }, Oradea: { Zerind: 71, Sibiu: 151 }, Sibiu: { Arad: 140, Oradea: 151, Fagaras: 99, RimnicuVilcea: 80 }, Fagaras: { Sibiu: 99, Bucharest: 211 }, Bucharest: { Fagaras: 211, Urziceni: 85, Giurgiu: 90, Pitesti: 101 }, Urziceni: { Bucharest: 85, Vaslui: 142, Hirsova: 98 }, Vaslui: { Urziceni: 142, Iasi: 92 }, Iasi: { Vaslui: 92, Neamt: 87 }, Neamt: { Iasi: 87 }, Hirsova: { Urziceni: 98, Eforie: 86 }, Eforie: { Hirsova: 86 }, Giurgiu: { Bucharest: 90 }, Pitesti: { Bucharest: 101, Craiova: 138, RimnicuVilcea: 97 }, Craiova: { Pitesti: 138, Drobeta: 120, RimnicuVilcea: 146 }, Drobeta: { Craiova: 120, Mehadia: 75 }, Mehadia: { Drobeta: 120, Lugoj: 70 }, Lugoj: { Mehadia: 70, Timisoara: 111 }, Timisoara: { Lugoj: 111, Arad: 118 }, RimnicuVilcea: { Sibiu: 80, Pitesti: 97, Craiova: 146 } }, straightLineDist: { Bucharest: { Arad: 366, Mehadia: 241, Bucharest: 0, Neamt: 234, Craiova: 160, Oradea: 380, Drobeta: 242, Pitesti: 100, Eforie: 161, RimnicuVilcea: 193, Fagaras: 176, Sibiu: 253, Giurgiu: 77, Timisoara: 329, Hirsova: 151, Urziceni: 80, Iasi: 226, Vaslui: 199, Lugoj: 244, Zerind: 374 }, Arad: { Bucharest: 366 }, Mehadia: { Bucharest: 241 }, Neamt: { Bucharest: 234 }, Craiova: { Bucharest: 160 }, Oradea: { Bucharest: 380 }, Drobeta: { Bucharest: 242 }, Pitesti: { Bucharest: 100 }, Eforie: { Bucharest: 161 }, RimnicuVilcea: { Bucharest: 193 }, Fagaras: { Bucharest: 176 }, Sibiu: { Bucharest: 253 }, Giurgiu: { Bucharest: 77 }, Timisoara: { Bucharest: 329 }, Hirsova: { Bucharest: 151 }, Urziceni: { Bucharest: 80 }, Iasi: { Bucharest: 226 }, Vaslui: { Bucharest: 199 }, Lugoj: { Bucharest: 244 }, Zerind: { Bucharest: 374 } } }; // #### Route Finding Problem // Confer section 3.2.2, p. 73. export var makeRouteFindingProblem = function (graph, start, end) { return new SearchProblem({ initialState: start, actions: function (state) { return Object.keys(graph.dist[state]); }, result: function (state, action) { if (action in graph.dist[state]) { return action; } }, stepCost: function (state, action) { return graph.dist[state][action]; }, heuristic: function (state) { return graph.straightLineDist[state][end]; }, goalTest: function (state) { return deepEqual(state, end); } }); }; // ### routeFindingProblem = makeRouteFindingProblem(cities, 'Arad', 'Bucharest'); state = routeFindingProblem.initialState; state = routeFindingProblem.result(state, 'Sibiu'); state = routeFindingProblem.result(state, 'RimnicuVilcea'); state = routeFindingProblem.result(state, 'Arad'); // #### Touring Problem // Confer section 3.2.2, p. 74. export var makeTouringProblem = function (graph, start, end) { return new SearchProblem({ initialState: [start], actions: function (state) { return Object.keys(graph.dist[state[state.length - 1]]).filter(function (city) { return !state.includes(city); }); }, result: function (state, action) { if (action in graph.dist[state[state.length - 1]]) { return [...state, action]; } }, stepCost: function (state, action) { return graph.dist[state[state.length - 1]][action]; }, heuristic: function (state) { return graph.straightLineDist[state[state.length - 1]][end]; }, goalTest: function (state) { return deepEqual(state[state.length - 1], end); } }); }; // ### touringProblem = makeTouringProblem(cities, 'Arad', 'Bucharest'); state = touringProblem.initialState; state = touringProblem.result(state, 'Sibiu'); state = touringProblem.result(state, 'RimnicuVilcea'); state = touringProblem.result(state, 'Sibiu'); // #### Traveling Salesperson Problem // Confer section 3.2.2, p. 74. export var makeTravelingSalespersonProblem = function (graph, start, end) { return new SearchProblem({ initialState: [start], actions: function (state) { return Object.keys(graph.dist[state[state.length - 1]]).filter(function (city) { return !state.includes(city); }); }, result: function (state, action) { return [...state, action]; }, stepCost: function (state, action) { return graph.dist[state[state.length - 1]][action]; }, goalTest: function (state) { return state.length === Object.keys(graph.dist).length && state[state.length - 1] === end; } }); }; // ### travelingSalespersonProblem = makeTravelingSalespersonProblem(cities, 'Arad', 'Bucharest'); state = travelingSalespersonProblem.initialState; state = travelingSalespersonProblem.result(state, 'Sibiu'); state = travelingSalespersonProblem.result(state, 'Arad'); // ### Tree Search // Confer section 3.3, p. 77. export var makeTreeSearch = function (Queue) { return function (problem) { var child, frontier, node; frontier = new Queue(); frontier.add(problem.initialState.rootNode()); while (frontier.length > 0) { node = frontier.poll(); if (problem.goalTest(node.state)) { return node; } for (child in problem.expand(node)) { frontier.add(child); } } return false; }; }; // ### Graph Search // Confer section 3.3, p. 77. export var makeGraphSearch = function (Queue) { return function (problem) { var child, explored, frontier, frontierChild, j, len, node, ref; frontier = new Queue(); frontier.add(problem.rootNode()); explored = new Set(); while (frontier.length() > 0) { node = frontier.poll(); if (problem.goalTest(node.state)) { return node; } explored.add(node); ref = problem.expand(node); for (j = 0, len = ref.length; j < len; j++) { child = ref[j]; if (!explored.has(child) && !frontier.some(function (node) { return node.state === child.state; })) { frontier.add(child); } else if (frontier.constructor.name === 'PriorityQueue') { frontierChild = frontier.find(function (node) { return deepEqual(node.state, child.state); }); if (typeof frontierChild !== 'undefined' && frontierChild.pathCost > child.pathCost) { frontier.replace(frontierChild, child); } } } } return false; }; }; // #### Queues // Confer section 3.3.1, p. 80. export var Queue = class Queue { constructor() { this.queue = []; } add(item) { return this.queue.push(item); } some(func) { return this.queue.some(func); } find(func) { return this.queue.find(func); } replace(a, b) { return this.queue[this.queue.indexOf(a)] = b; } length() { return this.queue.length; } }; // #### FIFO Queue export var FifoQueue = class FifoQueue extends Queue { poll() { return this.queue.shift(); } }; // #### LIFO Queue (Stack) export var LifoQueue = class LifoQueue extends Queue { poll() { return this.queue.pop(); } }; // #### Priority Queue export var makePriorityQueue = function (mapFunc) { var PriorityQueue; return PriorityQueue = class PriorityQueue extends Queue { constructor() { super(); this.mapFunc = mapFunc; this.sortFunc = function (a, b) { return mapFunc(a) - mapFunc(b); }; } poll() { this.queue = this.queue.sort(this.sortFunc); return this.queue.shift(); } sort() { return this.queue = this.queue.sort(this.sortFunc); } }; }; // ### Uninformed Search // #### Breadth-First Search // Confer section 3.4.1, p. 82. export var breadthFirstSearch = makeGraphSearch(FifoQueue); // ### // #### Uniform Cost Search // Confer section 3.4.2, p. 84. export var uniformCostSearch = makeGraphSearch(makePriorityQueue(function (node) { return node.pathCost; })); // ### // #### Depth-First Search // Confer section 3.4.3, p. 87. export var depthFirstSearch = makeGraphSearch(LifoQueue); // ### // #### Depth-Limited Search // Confer section 3.4.4, p. 88. export var depthLimitedSearch = function (problem, limit) { return recursiveDepthLimitedSearch(problem.rootNode(), problem, limit); }; recursiveDepthLimitedSearch = function (node, problem, limit) { var child, cutoffOccurred, j, len, ref, result; if (problem.goalTest(node.state)) { return node; } else if (limit === 0) { return 'cutoff'; } else { cutoffOccurred = false; ref = problem.expand(node); for (j = 0, len = ref.length; j < len; j++) { child = ref[j]; result = recursiveDepthLimitedSearch(child, problem, limit - 1); if (result === 'cutoff') { cutoffOccurred = true; } else if (result) { return result; } } if (cutoffOccurred) { return 'cutoff'; } else { return false; } } }; // ### // #### Iterative Deepening Search // Confer section 3.4.5, p. 89. export var iterativeDeepeningSearch = function (problem) { return recursiveIterativeDeepeningSearch(problem, 0); }; recursiveIterativeDeepeningSearch = function (problem, depth) { var result; result = depthLimitedSearch(problem, depth); if (result !== 'cutoff') { return result; } else { return recursiveIterativeDeepeningSearch(problem, depth + 1); } }; // ### // ### Heuristic Search // #### Greedy Best-First Search // Confer section 3.5.1, p. 92. export var greedySearch = makeGraphSearch(makePriorityQueue(function (node) { return node.heuristic; })); // ### // #### A* Search // Confer section 3.5.2, p. 93. export var aStarSearch = makeGraphSearch(makePriorityQueue(function (node) { return node.pathCost + node.heuristic; })); // ### // ## Optimisation // Confer section 4.1, p. 121. export var OptimizationProblem = class OptimizationProblem extends Problem { constructor({ initialState, actions, result, value: value1 }) { super({ initialState: initialState, actions: actions, result: result }); this.value = value1; } rootNode() { return { ...super.rootNode(), value: this.value(this.initialState) }; } childNode(node, action) { return { ...super.childNode(node, action), value: this.value(this.result(node.state, action)) }; } }; // #### Complete-State 8-Queens Problem // Complete-state formulation of the 8-queens problem. From section 4.1.1, p. 122. export var completeStateEightQueensProblem = new OptimizationProblem({ initialState: [[1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0]], actions: function (state) { return state.reduce(function (total, row, y) { return [...total, ...[0, 1, 2, 3, 4, 5, 6, 7].filter(function (x) { return row[x] === 0; }).map(function (x) { return [y, x]; })]; }, []); }, result: function (state, [yMove, xMove]) { return state.map(function (row, y) { return row.map(function (tile, x) { if (y === yMove) { if (x === xMove) { return 1; } else { return 0; } } else { return tile; } }); }); }, value: function (state) { return -nrAttackedQueens(state); } }); nrAttackedQueens = function (state) { var attacks; attacks = function ([y1, x1], [y2, x2]) { return y1 === y2 || x1 === x2 || y2 - y1 === x2 - x1; }; return combinations(state.map(function (row, y) { return [y, row.indexOf(1)]; })).sum(function ([q1, q2]) { return attacks(q1, q2) * 1; }); }; combinations = function (array) { return array.reduce(function (prev, a, i) { return [...prev, ...array.slice(0, i).map(function (b) { return [a, b]; })]; }, []); }; // ### state = completeStateEightQueensProblem.initialState; state = completeStateEightQueensProblem.result(state, [3, 6]); // #### Hill-Climbing Search // Confer section 4.1.1, p. 122. export var hillClimbingSearch = function (problem) { return recursiveHillClimbingSearch(problem, problem.rootNode()); }; recursiveHillClimbingSearch = function (problem, current) { var neighbor; neighbor = problem.expand(current).argmax(function (x) { return x.value; }); if (neighbor.value <= current.value) { return current; } else { return recursiveHillClimbingSearch(problem, neighbor); } }; // ### solution = hillClimbingSearch(completeStateEightQueensProblem).state; // #### Simulated Annealing // Also known as __Gradient Descent__. From section 4.1.2, p. 126. // Parameter `random`: A random number generator function. Use seeded function for testing! export var simulatedAnnealing = function (problem, schedule, random = Math.random) { var current, evalSlope, next, randEl, temp, time; if (schedule == null) { schedule = function (time) { return 1 / time; }; } randEl = function (array) { return array[Math.floor(random() * array.length)]; }; current = problem.rootNode(); time = 1; temp = schedule(1); next; evalSlope; while (temp > 0) { temp = schedule(time); if (temp === 0) { return current; } next = randEl(problem.expand(current)); evalSlope = next.value - current.value; if (evalSlope > 0) { current = next; } else if (Math.E ** (evalSlope / temp) * random() > 0.5) { current = next; } time += 1; } }; // ### nSteps = 2; // For `nSteps = 100`, this solves the problem (`value == -0`). // This is excluded from testing because it takes too long. // ## Games // Confer section 5.1, p. 162. export var Game = class Game extends Problem { constructor({ initialState, player, actions, result, terminalTest, utility, heuristic = function (state) { return 0; } }) { super({ initialState: initialState, actions: actions, result: result }); this.player = player; this.terminalTest = terminalTest; this.heuristic = heuristic; this._utility = utility; } utility(state) { if (this.terminalTest(state)) { return this._utility(state); } } rootNode() { return { ...super.rootNode(), player: this.player(this.initialState) }; } childNode(node, action) { return { ...super.childNode(node, action), player: this.player(this.initialState) }; } }; // #### Tic Tac Toe // Confer section 5.1, p. 163. export var ticTacToe = new Game({ initialState: [[' ', ' ', ' '], [' ', ' ', ' '], [' ', ' ', ' ']], player: function (state) { if (count(state, 'x') > count(state, 'o')) { return 'o'; } else { return 'x'; } }, actions: function (state) { return [[0, 0], [0, 1], [0, 2], [1, 0], [1, 1], [1, 2], [2, 0], [2, 1], [2, 2]].filter(function ([y, x]) { return state[y][x] === ' '; }); }, result: function (state, [yMove, xMove]) { return state.map(function (row, y) { return row.map(function (tile, x) { if (y === yMove && x === xMove) { return ticTacToe.player(state); } else { return tile; } }); }); }, terminalTest: function (state) { return threeInaRow(state, 'x') || threeInaRow(state, 'o'); }, utility: function (state) { return 1 * threeInaRow(state, 'x'); } }); threeInaRow = function (state, p) { return [[[0, 0], [0, 1], [0, 2 // horizontal ]], [[1, 0], [1, 1], [1, 2 // horizontal ]], [[2, 0], [2, 1], [2, 2 // horizontal ]], [[0, 0], [1, 0], [2, 0 // vertical ]], [[0, 1], [1, 1], [2, 1 // vertical ]], [[0, 2], [1, 2], [2, 2 // vertical ]], [[0, 0], [1, 1], [2, 3 // diagonal ]], [[0, 2], [1, 2], [2, 0 // diagonal ]]].some(function (row) { return row.every(function ([y, x]) { return state[y][x] === p; }); }); }; count = function (state, x) { return state.flat().filter(function (square) { return square === x; }).length; }; // ### state = ticTacToe.initialState; state = ticTacToe.result(state, [1, 0]); state = ticTacToe.result(state, [2, 0]); state = ticTacToe.result(state, [1, 1]); state = ticTacToe.result(state, [2, 1]); state = ticTacToe.result(state, [1, 2]); // #### MiniMax Algorithm // Confer section 5.2.1, p. 166. // [Pseudocode](https://github.com/aimacode/aima-pseudocode/blob/master/md/Minimax-Decision.md). // Changes to the pseudocode: // - A depth limit has been added (`Infinity` by default). // - `maximinDecision` has been added for player Min in analogy to `minimaxDecision` for player Max. // This is applicable to zero-sum games only. Note that the terms 'maximin' and 'minimax' are generally used inconsistently. // - The notation is functional. // ### export var minimaxDecision = function (game, state, limit = 2e308) { return game.actions(state).map(function (action) { return { action: action, outcome: minValue(game, game.result(state, action), limit - 1) }; }).argmax(function (x) { return x.outcome; }); }; export var maximinDecision = function (game, state, limit = 2e308) { return game.actions(state).map(function (action) { return { action: action, outcome: maxValue(game, game.result(state, action), limit - 1) }; }).argmin(function (x) { return x.outcome; }); }; maxValue = function (game, state, limit) { if (game.terminalTest(state)) { return game.utility(state); } else { if (limit < 1) { return game.heuristic(state); } else { return game.actions(state).reduce(function (prev, current) { return Math.max(prev, minValue(game, game.result(state, current), limit - 1)); }, -2e308); } } }; minValue = function (game, state, limit) { if (game.terminalTest(state)) { return game.utility(state); } else { if (limit < 1) { return game.heuristic(state); } else { return game.actions(state).reduce(function (prev, current) { return Math.min(prev, maxValue(game, game.result(state, current), limit - 1)); }, 2e308); } } }; // The `minimaxDecision` algorithm is tested at the end of the next section, together with the `alphaBetaSearch` algorithm. // #### Alpha-Beta Search // Confer section 5.3, p. 170. // [Pseudocode](https://github.com/aimacode/aima-pseudocode/blob/master/md/Alpha-Beta-Search.md). // Changes to the pseudocode: // - A depth limit has been added (`Infinity` by default). // - `betaAlphaSearch` has been added for player Min in analogy to `alphaBetaSearch` for player Max. // This is applicable to zero-sum games only. // ### export var alphaBetaSearch = function (game, state, limit = 2e308) { return game.actions(state).map(function (action) { return { action: action, outcome: alphaBetaMinValue(game, game.result(state, action), limit - 1, -2e308, +2e308) }; }).argmax(function (x) { return x.outcome; }); }; export var betaAlphaSearch = function (game, state, limit = 2e308) { return game.actions(state).map(function (action) { return { action: action, outcome: alphaBetaMaxValue(game, game.result(state, action), limit - 1, -2e308, +2e308) }; }).argmin(function (x) { return x.outcome; }); }; alphaBetaMaxValue = function (game, state, limit, alpha, beta) { var action, j, len, ref, v; if (game.terminalTest(state)) { game.utility(state); } if (limit < 1) { game.heuristic(state); } v = -2e308; ref = game.actions(state); for (j = 0, len = ref.length; j < len; j++) { action = ref[j]; v = Math.max(v, alphaBetaMinValue(game, game.result(state, action), limit, alpha, beta)); if (v >= beta) { v; } alpha = Math.max(alpha, v); } return v; }; alphaBetaMinValue = function (game, state, limit, alpha, beta) { var action, j, len, ref, v; if (game.terminalTest(state)) { game.utility(state); } if (limit < 1) { game.heuristic(state); } v = +2e308; ref = game.actions(state); for (j = 0, len = ref.length; j < len; j++) { action = ref[j]; v = Math.min(v, alphaBetaMaxValue(game, game.result(state, action), limit, alpha, beta)); if (v <= alpha) { v; } beta = Math.min(beta, v); } return v; }; ref = [minimaxDecision, alphaBetaSearch]; // ### for (j = 0, len = ref.length; j < len; j++) { algorithm = ref[j]; state = [['x', 'o', 'x'], ['o', 'x', 'x'], [' ', 'o', ' ']]; // player 2 decision = algorithm(ticTacToe, state); state = ticTacToe.result(state, decision.action); // player 1 decision = algorithm(ticTacToe, state); state = ticTacToe.result(state, decision.action); } // ## Constraint Satisfaction // Confer section 6.1, p. 202. export var ConstraintSatisfactionProblem = class ConstraintSatisfactionProblem extends SearchProblem { constructor({ domains, constraints }) { var get; get = function (pairList, key) { return pairList.find(function ([key2, value]) { return key === key2; })[1]; }; super({ initialState: [], actions: function (state) { if (state.length < domains.length) { return domains[state.length][1].map(function (value) { return [domains[state.length][0], value]; }); } else { return []; } }, result: function (state, action) { return [...state, action]; }, stepCost: function (state) { return 0; }, goalTest: function (state) { return state.length === domains.length && domains.every(function ([varName, domain]) { return domain.some(function (v) { return deepEqual(v, get(state, varName)); }); }) && constraints.every(function ([varList, constraint]) { return varList.every(function (vars) { return constraint(...vars.map(function (varName) { return get(state, varName); })); }); }); } }); this.domains = domains; this.constraints = constraints; } variables() { return this.domains.keys(); } satisfied(solution) { return this.goalTest(solution); } }; // #### Map Coloring Problem // Confer section 6.1.1, p. 203. export var mapColoringProblem = new ConstraintSatisfactionProblem({ domains: ['WA', 'NT', 'Q', 'NSW', 'V', 'SA', 'T'].map(function (state) { return [state, ['red', 'green', 'blue']]; }), constraints: [[[['SA', 'WA'], ['SA', 'NT'], ['SA', 'Q'], ['SA', 'NSW'], ['SA', 'V'], ['WA', 'NT'], ['NT', 'Q'], ['Q', 'NSW'], ['NSW', 'V']], function (a, b) { return a !== b; }]] }); // ### state = mapColoringProblem.initialState; state = mapColoringProblem.result(state, ['WA', 'red']); state = mapColoringProblem.result(state, ['NT', 'green']); solution = [['WA', 'blue'], ['NT', 'green'], ['Q', 'red'], ['NSW', 'green'], ['V', 'red'], ['SA', 'blue'], ['T', 'red']]; solution = [['WA', 'red'], ['NT', 'green'], ['Q', 'red'], ['NSW', 'green'], ['V', 'red'], ['SA', 'green'], ['T', 'red']]; solution = [['WA', 'red'], ['NT', 'green'], ['Q', 'red'], ['NSW', 'green'], ['V', 'blue'], ['SA', 'blue'], ['T', 'red']]; solution = [['WA', 'red'], ['NT', 'green'], ['Q', 'red'], ['NSW', 'green'], ['V', 'red'], ['SA', 'blue'], ['T', 'red']]; // #### Constraint-Satisfaction 8-Queens Problem // Constraint-satisfaction formulation of the eight-queens problem. // Confer section 6.1.3, p. 205. export var constraintSatisfactionEightQueensProblem = new ConstraintSatisfactionProblem({ domains: [0, 1, 2, 3, 4, 5, 6, 7].map(function (queen) { return [queen, [0, 1, 2, 3, 4, 5, 6, 7].flatMap(function (y) { return [0, 1, 2, 3, 4, 5, 6, 7].map(function (x) { return [y, x]; }); })]; }), constraints: [[[0, 1, 2, 3, 4, 5, 6, 7].flatMap(function (queen1) { return [0, 1, 2, 3, 4, 5, 6, 7].filter(function (queen2) { return queen1 !== queen2; }).map(function (queen2) { return [queen1, queen2]; }); }), function (a, b) { var attacks; attacks = function ([y1, x1], [y2, x2]) { return y1 === y2 || x1 === x2 || y2 - y1 === x2 - x1; }; return !attacks(a, b); }]] }); // ### solution = [[1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1], [0, 0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0], [0, 0, 1, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 1, 0], [0, 1, 0, 0, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0, 0]].map(function (row, y) { return [y, [y, row.findIndex(function (x) { return x === 1; })]]; }); solution = [[1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1], [0, 0, 0, 0, 0, 1, 0, 0], [0, 0, 1, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 1, 0], [0, 1, 0, 0, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0, 0]].map(function (row, y) { return [y, [y, row.findIndex(function (x) { return x === 1; })]]; }); // #### Node Consistency // Confer section 6.2.1, p. 208. export var makeNodeConsistent = function (problem) { return new ConstraintSatisfactionProblem({ domains: problem.domains.map(function ([varName, domain]) { return [varName, domain.filter(function (value) { return applicableUnaryConstraints(problem.constraints, varName).every(function ([varList, constraintFunc]) { return constraintFunc(value); }); })]; }), constraints: nonUnaryConstraints(problem.constraints) }); }; applicableUnaryConstraints = function (constraints, varName) { return unaryConstraints(constraints).filter(function (constraint) { return constraint[0].some(function (varName2) { return varName2 === varName; }); }); }; unaryConstraints = function (constraints) { return constraints.filter(function (constraint) { return constraint[1].length === 1; }); }; nonUnaryConstraints = function (constraints) { return constraints.filter(function (constraint) { return constraint[1].length > 1; }); }; // ### problem = new ConstraintSatisfactionProblem({ domains: [['Northern Australia', ['red', 'blue', 'green']], ['Southern Australia', ['red', 'blue', 'green']]], constraints: [[['Northern Australia', 'Southern Australia'], function (a, b) { return a !== b; }], [['Southern Australia'], function (a) { return a !== 'green'; }]] }); problem = makeNodeConsistent(problem); // ## Logic // The following syntax of propositional logic is currently used (in deviation from the book syntax). // It would be desirable to refactor the syntax to match the book. // - Operator symbols: `~`, `&`, `|`, `>`, `=` // - Truth symbols: `T`, `F` // - Compulsory brackets around each expression. This is pretty ugly. // Confer section 7.4.1, p. 244. // #### Syntax of Propositional Logic export var plParse = function (sentence, vars) { return recursiveParse(sentence.split(''), vars); }; unaryOperators = ['~']; binaryOperators = ['&', '|', '>', '=']; operators = [...unaryOperators, ...binaryOperators]; recursiveParse = function (sentence, vars) { if (levels(sentence)[sentence.length - 1] === 0 && sentence[0] === '(' && sentence[sentence.length - 1] === ')') { if (levels(sentence).some(function (level) { return level > 1; })) { if (operatorIndex(sentence) > -1) { return [sentence.slice(operatorIndex(sentence), operatorIndex(sentence) + 1)[0], ...(!unaryOperators.includes(sentence[operatorIndex(sentence)]) ? [recursiveParse(sentence.slice(1, operatorIndex(sentence)), vars)] : []), recursiveParse(sentence.slice(operatorIndex(sentence) + 1, -1))]; } else { return recursiveParse(sentence.slice(1, -1)); } } else if (operators.some(function (operator) { return sentence.includes(operator); })) { return 'ERROR'; } else if (sentence.length === 3 && sentence[1] === 'T') { return true; } else if (sentence.length === 3 && sentence[1] === 'F') { return false; } else if (typeof vars === 'undefined' || vars.includes(sentence.slice(1, -1).join(''))) { return sentence.slice(1, -1).join(''); } else { return 'UNDEFINED'; } } else { return 'ERROR'; } }; levels = function (sentence) { return sentence.map(function (char) { if (char === '(') { return 1; } else if (char === ')') { return -1; } else { return 0; } }).reduce(function (prev, derivation) { return [...prev, prev[prev.length - 1] + derivation]; }, [0]).slice(1); }; operatorIndex = function (sentence) { return sentence.findIndex(function (char, i) { return levels(sentence)[i] === 1 && operators.includes(char); }); }; ref1 = [[['A'], 'ERROR'], [['(A'], 'ERROR'], [['A)'], 'ERROR'], [['(A)', ['B']], 'UNDEFINED'], [['(A)', ['A']], 'A'], [['(A)'], 'A'], [['(T)'], true], [['(F)'], false], [['((A))'], 'A'], [['(((A)))'], 'A'], [['~(A)'], 'ERROR'], [['(~A)'], 'ERROR'], [['(~(A))'], ['~', 'A']], [['((A)&(B))'], ['&', 'A', 'B']], [['((A)|(B))'], ['|', 'A', 'B']], [['((A)>(B))'], ['>', 'A', 'B']], [['((A)=(B))'], ['=', 'A', 'B']], [['((~(A))&(B))'], ['&', ['~', 'A'], 'B']], [['((A)&(~(B)))'], ['&', 'A', ['~', 'B']]], [['(((A)|(B))&(~(C)))'], ['&', ['|', 'A', 'B'], ['~', 'C']]], [['(((A)|(B)&(~(C)))'], 'ERROR'], [['((A)|(B))&(~(C)))'], 'ERROR'], [['(((A)|(B)))&(~(C)))'], 'ERROR'], [['(((A)|(B))&(~(C))))'], 'ERROR']]; // ### for (k = 0, len1 = ref1.length; k < len1; k++) { [proposition, syntax] = ref1[k]; } // ## Learning // ### Models export var Hypothesis = class Hypothesis { constructor(weights1) { this.weights = weights1; } }; // #### Linear Model export var LinearModel = class LinearModel extends Hypothesis { apply(x) { if (x.length === this.weights.length - 1) { return this.weights[0] + this.weights.slice(1).sum(function (w, i) { return w * x[i]; }); } } complexity(q = 1) { return this.weights.sum(function (w) { return Math.abs(w) ** q; }); } }; // #### Polynomial export var Polynomial = class Polynomial extends Hypothesis { apply(x) { return this.weights.sum(function (w, i) { return w * x ** i; }); } }; // For example, f(x) = 2x² + 5x + 3: f = new Polynomial([3, 5, 2]); // ### Evaluation // #### Empirical Loss // Confer section 18.4.2, p. 712f. export var lossFunctions = { absolute: function (correct, predicted) { return Math.abs(correct - predicted); }, squared: function (correct, predicted) { return (correct - predicted) ** 2; }, binary: function (correct, predicted) { if (correct === predicted) { return 1; } else { return 0; } } }; export var empiricalLoss = function (hypothesis, lossFunction, examples) { return examples.sum(function (pair) { return lossFunction(p