Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/davidpomerenke/aima.js

Russell & Norvig, "Artificial Intelligence – A Modern Approach" in function-oriented CoffeeScript.
https://github.com/davidpomerenke/aima.js

aima artificial-intelligence coffeescript constraints games logics node optimization problem-solving search

Last synced: 18 days ago
JSON representation

Russell & Norvig, "Artificial Intelligence – A Modern Approach" in function-oriented CoffeeScript.

Awesome Lists containing this project

README

        

# 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

import deepEqual from 'deep-equal'
import { strict as assert } from 'assert'

For testing subroutines which depend on random numbers, we use a seeded random number generator which can be called by `rand.random()`:

import gen from 'random-seed' # testing only
seed = 'seedshrdlu4523' # testing only
rand = gen.create seed # testing only

## Utilities

#### Sum

Array::sum = (f = (x) -> x) ->
this.reduce (accumulator, element, index, array) ->
accumulator + f element, index, array
, 0
###

array = [ [100, 1000], [200, 2000], [300, 3000] ]
assert.equal (array.sum (x) -> x[1]), 6000
assert.equal (array.sum (x, i) -> x[1] + 10 * i), 6030
assert.equal (array.sum (x, i, array) -> x[1] + 10 * i + array[0][0]), 6330

#### Argmax, Argmin

Array::argmin = (f) ->
this.reduce (accumulator, element) ->
if (f element) < f accumulator
element
else
accumulator
###

Array::argmax = (f) ->
this.reduce (accumulator, element) ->
if (f element) > f accumulator
element
else
accumulator
###

array = [ [100, 3000], [200, 1000], [300, 2000] ]
assert.deepEqual (array.argmax (x) -> x[0]), [300, 2000]
assert.deepEqual (array.argmax (x) -> x[1]), [100, 3000]
assert.deepEqual (array.argmin (x) -> x[0]), [100, 3000]
assert.deepEqual (array.argmin (x) -> x[1]), [200, 1000]

#### Max, Min

Array::min = ->
this.argmin (x) -> x
###

Array::max = ->
this.argmax (x) -> x
###

array = [3, 5, 4, 1, 2]
assert.equal array.max(), 5
assert.equal array.min(), 1

#### Factorial

factorial = (n) ->
[1..n]
.reduce (accumulator, current) ->
accumulator * current
###

assert.equal factorial(5), 1 * 2 * 3 * 4 * 5
assert.equal factorial(10), factorial(5) * 6 * 7 * 8 * 9 * 10

## Intelligent Agents

#### Table-Driven Agent

Confer section 2.4, p. 47.

export class TableDrivenAgent
constructor: (@table = {}) ->
@percepts = []

action: (percept) ->
@percepts.push percept
@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 class SimpleReflexAgent
constructor: (@rules) ->

action: (percept) ->
state = @interpretInput percept
rule = @rules.find (rule) ->
rule.condition ...state
rule?.action

interpretInput: (percept) ->
percept

Example: __Reflex Vacuum Agent__

reflexVacuumAgent = new SimpleReflexAgent [
condition: ([..., status ]) -> status == 'dirty'
action: 'suck'
,
condition: ([location, ...]) -> location == 'A'
action: 'right'
,
condition: ([location, ...]) -> 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 class Problem
constructor: ({ @initialState, @actions, result }) ->
@_result = result

result: (state, action) ->
if @actions(state).some (a) -> deepEqual action, a
@_result state, action

rootNode: ->
state: @initialState

childNode: (node, action) -> {
state: @result node.state, action
parent: node
action: action
}

expand: (node) ->
@childNode(node, action) for action in @actions(node.state)

@solutionPath: (node) ->
if 'parent' of node
[...Problem.solutionPath(node.parent), node.state]
else
[node.state]

## Search

Confer section 3.1.1, p. 67.

export class SearchProblem extends Problem
constructor: ({ initialState, actions, result, stepCost, @heuristic = (-> 0), @goalTest }) ->
super
initialState: initialState,
actions: actions,
result: result
@_stepCost = stepCost

stepCost: (state, action) ->
this._stepCost state, action if (@actions state).some (a) -> deepEqual action, a

rootNode: -> {
...super()
pathCost: 0
heuristic: @heuristic @initialState
}

childNode: (node, action) -> {
...super node, action
pathCost: node.pathCost + @stepCost node.state, action
heuristic: @heuristic @result node.state, action
}

### Toy Problems

#### Vacuum World

Confer section 3.2.1, p. 70.

export vacuumWorld = new SearchProblem
initialState:
location: 'A'
A: 'dirty'
B: 'dirty'
actions: (state) -> [
'left'
'right'
'suck'
]
result: (state, action) ->
location:
if action == 'left'
'A'
else if action == 'right'
'B'
else
state.location
A:
if state.location == 'A' and action == 'suck'
'clean'
else
state.A
B:
if state.location == 'B' and action == 'suck'
'clean'
else
state.B
stepCost: (state, action) -> 1
goalTest: (state) ->
state.A == 'clean' and
state.B == 'clean'
###

state = vacuumWorld.initialState
assert.deepEqual state, { location: 'A', A: 'dirty', B: 'dirty' }

state = vacuumWorld.result state, 'suck'
assert.deepEqual state, { location: 'A', A: 'clean', B: 'dirty' }

state = vacuumWorld.result state, 'suck'
assert.deepEqual state, { location: 'A', A: 'clean', B: 'dirty' }

state = vacuumWorld.result state, 'left'
assert.deepEqual state, { location: 'A', A: 'clean', B: 'dirty' }

state = vacuumWorld.result state, 'right'
assert.deepEqual state, { location: 'B', A: 'clean', B: 'dirty' }
assert not vacuumWorld.goalTest state

state = vacuumWorld.result state, 'suck'
assert.deepEqual state, { location: 'B', A: 'clean', B: 'clean' }
assert vacuumWorld.goalTest state

#### 8-Puzzle

Confer section 3.2.1, p. 71.

export makeEightPuzzle = (initialState) ->
moveIsValid = (zero) ->
zero.y in [0, 1, 2] and
zero.x in [0, 1, 2]

zero = (state) -> # position of zero
y: state.indexOf(state.filter((row) -> row.includes 0)[0])
x: state.filter( (row) -> row.includes 0)[0].indexOf 0

goalPosition = (nr) -> [
goalState.findIndex (row) -> row.includes(nr)
goalState.find( (row) -> row.includes(nr)).indexOf nr
]

manhattanDist = ([y1, x1], [y2, x2]) ->
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]
]

new SearchProblem
initialState: initialState
actions: (state) ->
Object.keys(moves)
.filter (key) -> moveIsValid
y: (zero state).y + moves[key].y
x: (zero state).x + moves[key].x
result: (state, action) ->
state.map (row, y) ->
row.map (nr, x) ->
# Shift zero to new position.
if y == (zero state).y + moves[action].y and
x == (zero state).x + moves[action].x
0
# Shift number to old position of zero.
else if nr == 0
state[(zero state).y + moves[action].y][(zero state).x + moves[action].x]
# Keep all other numbers.
else
nr
stepCost: (state, action) -> 1
heuristic: (state) ->
state.sum (numbers, y) ->
numbers.sum (number, x) ->
manhattanDist [y, x], goalPosition number
goalTest: (state) ->
deepEqual state, goalState
###

simpleEightPuzzle = makeEightPuzzle [
[1, 4, 2]
[3, 0, 5]
[6, 7, 8]
]

state = simpleEightPuzzle.initialState
assert.deepEqual state, [
[1, 4, 2]
[3, 0, 5]
[6, 7, 8]
]
assert.equal (simpleEightPuzzle.heuristic state), 4

state = simpleEightPuzzle.result state, 'up'
assert.deepEqual state, [
[1, 0, 2]
[3, 4, 5]
[6, 7, 8]
]
assert.equal (simpleEightPuzzle.heuristic state), 2
assert not simpleEightPuzzle.goalTest state

state = simpleEightPuzzle.result state, 'left'
assert.deepEqual state, [
[0, 1, 2]
[3, 4, 5]
[6, 7, 8]
]
assert.equal (simpleEightPuzzle.heuristic state), 0
assert simpleEightPuzzle.goalTest state
###

complexEightPuzzle = makeEightPuzzle [
[7, 2, 4]
[5, 0, 6]
[8, 3, 1]
]
state = complexEightPuzzle.initialState
assert.equal (simpleEightPuzzle.heuristic state), 20

#### Incremental 8-Queens Problem

Incremental formulation of the 8-queens problem. Confer section 3.2.1, p. 72.

export incrementalEightQueensProblem = new SearchProblem
initialState: []
actions: (state) ->
y = state.length
if y < 8
[0, 1, 2, 3, 4, 5, 6, 7]
.filter (x) ->
not isAttacked [y, x], state
else []
result: (state, action) -> [...state, action]
stepCost: (state, action) -> 0
goalTest: (state) -> state.length == 8

isAttacked = ([y0, x0], state) ->
state.some (x, y) ->
x == x0 or
y == y0 or
Math.abs(y - y0) == Math.abs(x - x0)
###

state = incrementalEightQueensProblem.initialState

state = incrementalEightQueensProblem.result state, 3
assert.deepEqual state, [3]
assert.deepEqual (incrementalEightQueensProblem.result state, 3), undefined

state = incrementalEightQueensProblem.result state, 5
assert.deepEqual state, [3, 5]
assert.deepEqual (incrementalEightQueensProblem.result state, 1), undefined

#### Knuth Conjecture

Confer section 3.2.1, p. 73.

export makeKnuthConjecture = (goal) ->
calc = (state) ->
state.reduce (total, operation) ->
if operation.isNumber
operation
else if operation == 'factorial'
factorial total
else if operation == 'square_root'
Math.sqrt total
else if operation == 'floor'
Math.floor total
new SearchProblem
initialState: [4]
actions: (state) ->
if Number.isInteger calc(state)
['square_root', 'factorial']
else
['square_root', 'floor']
result: (state, action) -> [...state, action],
stepCost: (state, action) -> 1,
goalTest: (state) -> (calc state) == goal
###

simpleKnuthConjecture = makeKnuthConjecture 1
complexKnuthConjecture = makeKnuthConjecture 5

state = complexKnuthConjecture.initialState
assert.deepEqual state, [4]

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'
assert not complexKnuthConjecture.goalTest state

state = complexKnuthConjecture.result state, 'floor'
assert complexKnuthConjecture.goalTest state

### 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 makeRouteFindingProblem = (graph, start, end) ->
new SearchProblem
initialState: start
actions: (state) ->
Object.keys graph.dist[state]
result: (state, action) ->
action if action of graph.dist[state]
stepCost: (state, action) ->
graph.dist[state][action]
heuristic: (state) ->
graph.straightLineDist[state][end]
goalTest: (state) ->
deepEqual state, end
###

routeFindingProblem = makeRouteFindingProblem cities, 'Arad', 'Bucharest'

state = routeFindingProblem.initialState
assert.equal state, 'Arad'
assert.equal (routeFindingProblem.stepCost state, 'Sibiu'), 140

state = routeFindingProblem.result state, 'Sibiu'
assert.equal state, 'Sibiu'
assert.equal (routeFindingProblem.stepCost state, 'RimnicuVilcea'), 80

state = routeFindingProblem.result state, 'RimnicuVilcea'
assert.equal state, 'RimnicuVilcea'
assert.equal (routeFindingProblem.stepCost state, 'Arad'), undefined

state = routeFindingProblem.result state, 'Arad'
assert.equal state, undefined

#### Touring Problem

Confer section 3.2.2, p. 74.

export makeTouringProblem = (graph, start, end) ->
new SearchProblem
initialState: [start]
actions: (state) ->
Object.keys graph.dist[state[state.length - 1]]
.filter (city) -> not state.includes city
result: (state, action) ->
if action of graph.dist[state[state.length - 1]]
[...state, action]
stepCost: (state, action) ->
graph.dist[state[state.length - 1]][action]
heuristic: (state) ->
graph.straightLineDist[state[state.length - 1]][end]
goalTest: (state) ->
deepEqual state[state.length - 1], end
###

touringProblem = makeTouringProblem cities, 'Arad', 'Bucharest'

state = touringProblem.initialState
assert.deepEqual state, ['Arad']
assert.equal (touringProblem.stepCost state, 'Sibiu'), 140

state = touringProblem.result state, 'Sibiu'
assert.deepEqual state, ['Arad', 'Sibiu']
assert.equal (touringProblem.stepCost state, 'RimnicuVilcea'), 80

state = touringProblem.result state, 'RimnicuVilcea'
assert.deepEqual state, ['Arad', 'Sibiu', 'RimnicuVilcea']
assert.equal (touringProblem.stepCost state, 'Sibiu'), undefined

state = touringProblem.result state, 'Sibiu'
assert.deepEqual state, undefined

#### Traveling Salesperson Problem

Confer section 3.2.2, p. 74.

export makeTravelingSalespersonProblem = (graph, start, end) ->
new SearchProblem
initialState: [start]
actions: (state) ->
Object.keys graph.dist[state[state.length - 1]]
.filter (city) -> not state.includes city
result: (state, action) -> [...state, action]
stepCost: (state, action) ->
graph.dist[state[state.length - 1]][action]
goalTest: (state) ->
state.length == (Object.keys graph.dist).length and
state[state.length - 1] == end
###

travelingSalespersonProblem = makeTravelingSalespersonProblem cities, 'Arad', 'Bucharest'

state = travelingSalespersonProblem.initialState
assert.deepEqual state, ['Arad']
assert.equal (travelingSalespersonProblem.stepCost state, 'Sibiu'), 140

state = travelingSalespersonProblem.result state, 'Sibiu'
assert.deepEqual state, ['Arad', 'Sibiu']
assert.equal (travelingSalespersonProblem.stepCost state, 'Arad'), undefined

state = travelingSalespersonProblem.result state, 'Arad'
assert.deepEqual state, undefined

### Tree Search

Confer section 3.3, p. 77.

export makeTreeSearch = (Queue) ->
(problem) ->
frontier = new Queue
frontier.add problem.initialState.rootNode()
while frontier.length > 0
node = frontier.poll()
if problem.goalTest node.state
return node
(frontier.add child) for child of problem.expand node
false

### Graph Search

Confer section 3.3, p. 77.

export makeGraphSearch = (Queue) ->
(problem) ->
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
for child in problem.expand node
if (not explored.has child) and
not frontier.some (node) -> node.state == child.state
frontier.add child
else if frontier.constructor.name == 'PriorityQueue'
frontierChild = frontier
.find (node) -> deepEqual node.state, child.state
if typeof frontierChild != 'undefined' and
frontierChild.pathCost > child.pathCost
frontier.replace frontierChild, child
false

#### Queues

Confer section 3.3.1, p. 80.

export class Queue
constructor: ->
@queue = []

add: (item) ->
@queue.push item

some: (func) ->
@queue.some func

find: (func) ->
@queue.find func

replace: (a, b) ->
@queue[@queue.indexOf a] = b

length: ->
@queue.length

#### FIFO Queue

export class FifoQueue extends Queue
poll: ->
@queue.shift()

#### LIFO Queue (Stack)

export class LifoQueue extends Queue
poll: ->
@queue.pop()

#### Priority Queue

export makePriorityQueue = (mapFunc) ->
class PriorityQueue extends Queue
constructor: ->
super()
@mapFunc = mapFunc
@sortFunc = (a, b) -> mapFunc(a) - mapFunc(b)

poll: ->
@queue = @queue.sort @sortFunc
@queue.shift()

sort: ->
@queue = @queue.sort @sortFunc

### Uninformed Search

#### Breadth-First Search

Confer section 3.4.1, p. 82.

export breadthFirstSearch = makeGraphSearch FifoQueue
###

assert.deepEqual (Problem.solutionPath breadthFirstSearch vacuumWorld), [
{ location: 'A', A: 'dirty', B: 'dirty' }
{ location: 'A', A: 'clean', B: 'dirty' }
{ location: 'B', A: 'clean', B: 'dirty' }
{ location: 'B', A: 'clean', B: 'clean' }]
assert.deepEqual (Problem.solutionPath breadthFirstSearch simpleEightPuzzle), [
[ [ 1, 4, 2 ]
[ 3, 0, 5 ]
[ 6, 7, 8 ] ]
[ [ 1, 0, 2 ]
[ 3, 4, 5 ]
[ 6, 7, 8 ] ]
[ [ 0, 1, 2 ]
[ 3, 4, 5 ]
[ 6, 7, 8 ] ] ]
assert.deepEqual (breadthFirstSearch incrementalEightQueensProblem).state, [
[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 (row) -> row.indexOf 1
assert.deepEqual (breadthFirstSearch simpleKnuthConjecture).state, \
[4, 'square_root', 'square_root', 'floor']
assert.deepEqual (Problem.solutionPath breadthFirstSearch routeFindingProblem), \
[ 'Arad', 'Sibiu', 'Fagaras', 'Bucharest' ]
assert.deepEqual (breadthFirstSearch touringProblem).state, \
[ 'Arad', 'Sibiu', 'Fagaras', 'Bucharest' ]
assert.equal (breadthFirstSearch travelingSalespersonProblem), false

#### Uniform Cost Search

Confer section 3.4.2, p. 84.

export uniformCostSearch = makeGraphSearch makePriorityQueue (node) -> node.pathCost
###

assert.deepEqual (Problem.solutionPath uniformCostSearch vacuumWorld), [
{ location: 'A', A: 'dirty', B: 'dirty' }
{ location: 'A', A: 'clean', B: 'dirty' }
{ location: 'B', A: 'clean', B: 'dirty' }
{ location: 'B', A: 'clean', B: 'clean' }]
assert.deepEqual (Problem.solutionPath uniformCostSearch simpleEightPuzzle), [
[ [ 1, 4, 2 ]
[ 3, 0, 5 ]
[ 6, 7, 8 ] ]
[ [ 1, 0, 2 ]
[ 3, 4, 5 ]
[ 6, 7, 8 ] ]
[ [ 0, 1, 2 ]
[ 3, 4, 5 ]
[ 6, 7, 8 ] ]]
assert.deepEqual (uniformCostSearch incrementalEightQueensProblem).state, [
[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 (row) -> row.indexOf 1
assert.deepEqual (uniformCostSearch simpleKnuthConjecture).state, \
[4, 'square_root', 'square_root', 'floor']
assert.deepEqual (Problem.solutionPath uniformCostSearch routeFindingProblem), \
[ 'Arad', 'Sibiu', 'RimnicuVilcea', 'Pitesti', 'Bucharest' ]
assert.deepEqual (uniformCostSearch touringProblem).state, \
[ 'Arad', 'Sibiu', 'RimnicuVilcea', 'Pitesti', 'Bucharest' ]
assert.equal (uniformCostSearch travelingSalespersonProblem), false

#### Depth-First Search

Confer section 3.4.3, p. 87.

export depthFirstSearch = makeGraphSearch LifoQueue
###

assert.deepEqual (depthFirstSearch incrementalEightQueensProblem).state, [
[0, 0, 0, 0, 0, 0, 0, 1]
[0, 0, 0, 1, 0, 0, 0, 0]
[1, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 1, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 1, 0, 0]
[0, 1, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 1, 0]
[0, 0, 0, 0, 1, 0, 0, 0]
].map (row) -> row.indexOf 1
assert.deepEqual (depthFirstSearch touringProblem).state, [
'Arad',
'Timisoara',
'Lugoj',
'Mehadia',
'Drobeta',
'Craiova',
'RimnicuVilcea',
'Pitesti',
'Bucharest'
] # depends on the (arbitrary) order of attributes in the cities graph
assert.equal (depthFirstSearch travelingSalespersonProblem), false

#### Depth-Limited Search

Confer section 3.4.4, p. 88.

export depthLimitedSearch = (problem, limit) ->
recursiveDepthLimitedSearch problem.rootNode(), problem, limit

recursiveDepthLimitedSearch = (node, problem, limit) ->
if problem.goalTest node.state
node
else if limit == 0
'cutoff'
else
cutoffOccurred = false
for child in problem.expand node
result = recursiveDepthLimitedSearch child, problem, limit - 1
if result == 'cutoff'
cutoffOccurred = true
else if result
return result
if cutoffOccurred
'cutoff'
else
false
###

assert.equal (depthLimitedSearch vacuumWorld, 2), 'cutoff'
assert.deepEqual (Problem.solutionPath (depthLimitedSearch vacuumWorld, 3)), [
{ location: 'A', A: 'dirty', B: 'dirty' }
{ location: 'A', A: 'clean', B: 'dirty' }
{ location: 'B', A: 'clean', B: 'dirty' }
{ location: 'B', A: 'clean', B: 'clean' }]

assert.equal (depthLimitedSearch simpleEightPuzzle, 1), 'cutoff'
assert simpleEightPuzzle.goalTest (depthLimitedSearch simpleEightPuzzle, 2).state

assert.deepEqual (depthLimitedSearch incrementalEightQueensProblem, 8).state, [
[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 (row) -> row.indexOf 1

assert.equal (depthLimitedSearch simpleKnuthConjecture, 2), 'cutoff'
assert (depthLimitedSearch simpleKnuthConjecture, 3).state, 1

assert.deepEqual (Problem.solutionPath (depthLimitedSearch routeFindingProblem, 4)), \
['Arad', 'Sibiu', 'Fagaras', 'Bucharest']

assert.deepEqual (depthLimitedSearch touringProblem, 4).state, \
['Arad', 'Sibiu', 'Fagaras', 'Bucharest']

assert.equal (depthLimitedSearch travelingSalespersonProblem, 20), false

#### Iterative Deepening Search

Confer section 3.4.5, p. 89.

export iterativeDeepeningSearch = (problem) ->
recursiveIterativeDeepeningSearch problem, 0

recursiveIterativeDeepeningSearch = (problem, depth) ->
result = depthLimitedSearch problem, depth
if result != 'cutoff'
result
else
recursiveIterativeDeepeningSearch problem, (depth + 1)
###

assert.deepEqual (Problem.solutionPath iterativeDeepeningSearch vacuumWorld), [
{ location: 'A', A: 'dirty', B: 'dirty' }
{ location: 'A', A: 'clean', B: 'dirty' }
{ location: 'B', A: 'clean', B: 'dirty' }
{ location: 'B', A: 'clean', B: 'clean' }]
assert.deepEqual (Problem.solutionPath iterativeDeepeningSearch simpleEightPuzzle), [
[ [ 1, 4, 2 ]
[ 3, 0, 5 ]
[ 6, 7, 8 ] ]
[ [ 1, 0, 2 ]
[ 3, 4, 5 ]
[ 6, 7, 8 ] ]
[ [ 0, 1, 2 ]
[ 3, 4, 5 ]
[ 6, 7, 8 ] ]]
assert.deepEqual (iterativeDeepeningSearch incrementalEightQueensProblem).state, [
[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 (row) -> row.indexOf 1
assert.deepEqual (iterativeDeepeningSearch simpleKnuthConjecture).state, \
[4, 'square_root', 'square_root', 'floor']
assert.deepEqual (Problem.solutionPath iterativeDeepeningSearch routeFindingProblem), \
[ 'Arad', 'Sibiu', 'Fagaras', 'Bucharest' ]
assert.deepEqual (iterativeDeepeningSearch touringProblem).state, \
[ 'Arad', 'Sibiu', 'Fagaras', 'Bucharest' ]
assert.equal (iterativeDeepeningSearch travelingSalespersonProblem), false

### Heuristic Search

#### Greedy Best-First Search

Confer section 3.5.1, p. 92.

export greedySearch = makeGraphSearch \
makePriorityQueue (node) -> node.heuristic
###

assert.deepEqual (Problem.solutionPath greedySearch vacuumWorld), [
{ location: 'A', A: 'dirty', B: 'dirty' }
{ location: 'A', A: 'clean', B: 'dirty' }
{ location: 'B', A: 'clean', B: 'dirty' }
{ location: 'B', A: 'clean', B: 'clean' }]
assert.deepEqual (Problem.solutionPath greedySearch simpleEightPuzzle), [
[ [ 1, 4, 2 ]
[ 3, 0, 5 ]
[ 6, 7, 8 ] ]
[ [ 1, 0, 2 ]
[ 3, 4, 5 ]
[ 6, 7, 8 ] ]
[ [ 0, 1, 2 ]
[ 3, 4, 5 ]
[ 6, 7, 8 ] ]]
assert.deepEqual (greedySearch incrementalEightQueensProblem).state, [
[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 (row) -> row.indexOf 1
assert.deepEqual (greedySearch simpleKnuthConjecture).state, \
[4, 'square_root', 'square_root', 'floor']
assert.deepEqual (Problem.solutionPath greedySearch routeFindingProblem), \
[ 'Arad', 'Sibiu', 'Fagaras', 'Bucharest' ]
assert.deepEqual (greedySearch touringProblem).state, \
[ 'Arad', 'Sibiu', 'Fagaras', 'Bucharest' ]
assert.equal (greedySearch travelingSalespersonProblem), false

#### A* Search

Confer section 3.5.2, p. 93.

export aStarSearch = makeGraphSearch \
makePriorityQueue (node) -> node.pathCost + node.heuristic
###

assert.deepEqual (Problem.solutionPath aStarSearch vacuumWorld), [
{ location: 'A', A: 'dirty', B: 'dirty' }
{ location: 'A', A: 'clean', B: 'dirty' }
{ location: 'B', A: 'clean', B: 'dirty' }
{ location: 'B', A: 'clean', B: 'clean' }]
assert.deepEqual (Problem.solutionPath aStarSearch simpleEightPuzzle), [
[ [ 1, 4, 2 ]
[ 3, 0, 5 ]
[ 6, 7, 8 ] ]
[ [ 1, 0, 2 ]
[ 3, 4, 5 ]
[ 6, 7, 8 ] ]
[ [ 0, 1, 2 ]
[ 3, 4, 5 ]
[ 6, 7, 8 ] ]]
assert.deepEqual (aStarSearch incrementalEightQueensProblem).state, [
[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 (row) -> row.indexOf 1
assert.deepEqual (aStarSearch simpleKnuthConjecture).state, \
[4, 'square_root', 'square_root', 'floor']
assert.deepEqual (Problem.solutionPath aStarSearch routeFindingProblem), \
[ 'Arad', 'Sibiu', 'RimnicuVilcea', 'Pitesti', 'Bucharest' ]
assert.deepEqual (aStarSearch touringProblem).state, \
[ 'Arad', 'Sibiu', 'RimnicuVilcea', 'Pitesti', 'Bucharest' ]
assert.equal (aStarSearch travelingSalespersonProblem), false

## Optimisation

Confer section 4.1, p. 121.

export class OptimizationProblem extends Problem
constructor: ({ initialState, actions, result, @value }) ->
super
initialState: initialState,
actions: actions,
result: result

rootNode: -> {
...super()
value: @value @initialState
}

childNode: (node, action) -> {
...super node, action
value: @value @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 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: (state) ->
state.reduce (total, row, y) ->
[
...total,
...[0, 1, 2, 3, 4, 5, 6, 7]
.filter (x) -> row[x] == 0
.map (x) -> [y, x]
]
, []
result: (state, [yMove, xMove]) ->
state.map (row, y) ->
row.map (tile, x) ->
if y == yMove
if x == xMove
1
else
0
else
tile
value: (state) -> -nrAttackedQueens state

nrAttackedQueens = (state) ->
attacks = ([y1, x1], [y2, x2]) ->
y1 == y2 or
x1 == x2 or
y2 - y1 == x2 - x1
combinations \
state.map (row, y) ->
[y, row.indexOf 1]
.sum ([q1, q2]) ->
attacks(q1, q2) * 1

combinations = (array) ->
array.reduce (prev, a, i) ->
[
...prev,
...array
.slice 0, i
.map (b) -> [a, b]
]
, []
###

state = completeStateEightQueensProblem.initialState
assert.equal (completeStateEightQueensProblem.actions state).length, 8 * (8 - 1)
assert.equal (completeStateEightQueensProblem.value state), -(8**2 - 8) / 2

state = completeStateEightQueensProblem.result state, [3, 6]
assert.deepEqual state, [
[1, 0, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 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]
]
assert.equal (completeStateEightQueensProblem.value state), -(28 - 7)

#### Hill-Climbing Search

Confer section 4.1.1, p. 122.

export hillClimbingSearch = (problem) ->
recursiveHillClimbingSearch problem, problem.rootNode()

recursiveHillClimbingSearch = (problem, current) ->
neighbor = (problem.expand current)
.argmax (x) -> x.value
if neighbor.value <= current.value
current
else
recursiveHillClimbingSearch problem, neighbor
###

solution = (hillClimbingSearch completeStateEightQueensProblem).state
assert.deepEqual solution, [
[0, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 0],
[1, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0]
]

#### 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 simulatedAnnealing = (problem, schedule, random = Math.random) ->
schedule ?= (time) -> 1 / time
randEl = (array) -> 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
assert.equal \
(simulatedAnnealing completeStateEightQueensProblem, ((t) -> 1/t - 1/nSteps), rand.random).value, \
-22

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 class Game extends Problem
constructor: ({
initialState, @player, actions, result, @terminalTest, utility, @heuristic = ((state) -> 0)
}) ->
super
initialState: initialState
actions: actions
result: result
@_utility = utility

utility: (state) ->
@_utility state if @terminalTest state

rootNode: -> {
...super()
player: @player @initialState
}

childNode: (node, action) -> {
...super node, action
player: @player @initialState
}

#### Tic Tac Toe

Confer section 5.1, p. 163.

export ticTacToe = new Game
initialState: [
[' ', ' ', ' ']
[' ', ' ', ' ']
[' ', ' ', ' ']
]
player: (state) ->
if (count state, 'x') > (count state, 'o')
'o'
else
'x'
actions: (state) -> [
[0, 0], [0, 1], [0, 2]
[1, 0], [1, 1], [1, 2]
[2, 0], [2, 1], [2, 2]
].filter ([y, x]) ->
state[y][x] == ' '
result: (state, [yMove, xMove]) ->
state.map (row, y) ->
row.map (tile, x) ->
if y == yMove and x == xMove
ticTacToe.player state
else
tile
terminalTest: (state) ->
(threeInaRow state, 'x') or
(threeInaRow state, 'o')
utility: (state) ->
1 * (threeInaRow state, 'x')

threeInaRow = (state, p) -> [
[ [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 (row) ->
row.every ([y, x]) ->
state[y][x] == p

count = (state, x) ->
state
.flat()
.filter (square) -> square == x
.length
###

state = ticTacToe.initialState

state = ticTacToe.result state, [1, 0]
assert.deepEqual state, [
[' ', ' ', ' ']
['x', ' ', ' ']
[' ', ' ', ' ']
]

state = ticTacToe.result state, [2, 0]
assert.deepEqual state, [
[' ', ' ', ' ']
['x', ' ', ' ']
['o', ' ', ' ']
]

state = ticTacToe.result state, [1, 1]
assert.deepEqual state, [
[' ', ' ', ' ']
['x', 'x', ' ']
['o', ' ', ' ']
]

state = ticTacToe.result state, [2, 1]
assert.deepEqual state, [
[' ', ' ', ' ']
['x', 'x', ' ']
['o', 'o', ' ']
]

assert not ticTacToe.terminalTest state
assert.equal ticTacToe.utility(state), undefined

state = ticTacToe.result state, [1, 2]
assert.deepEqual state, [
[' ', ' ', ' ']
['x', 'x', 'x']
['o', 'o', ' ']
]
assert ticTacToe.terminalTest state
assert.equal ticTacToe.utility(state), 1

#### 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 minimaxDecision = (game, state, limit = Infinity) ->
game.actions state
.map (action) ->
action: action
outcome: minValue game, (game.result state, action), limit - 1
.argmax (x) -> x.outcome

export maximinDecision = (game, state, limit = Infinity) ->
game.actions state
.map (action) ->
action: action
outcome: maxValue game, (game.result state, action), limit - 1
.argmin (x) -> x.outcome

maxValue = (game, state, limit) ->
if game.terminalTest state
game.utility state
else
if limit < 1
game.heuristic state
else
game.actions state
.reduce (prev, current) ->
Math.max prev, (minValue game, (game.result state, current), limit - 1)
, -Infinity

minValue = (game, state, limit) ->
if game.terminalTest state
game.utility state
else
if limit < 1
game.heuristic state
else
game.actions state
.reduce (prev, current) ->
Math.min prev, (maxValue game, (game.result state, current), limit - 1)
, Infinity

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 alphaBetaSearch = (game, state, limit = Infinity) ->
game.actions state
.map (action) ->
action: action,
outcome: alphaBetaMinValue game, (game.result state, action), limit - 1, -Infinity, +Infinity
.argmax (x) -> x.outcome

export betaAlphaSearch = (game, state, limit = Infinity) ->
game.actions state
.map (action) ->
action: action,
outcome: alphaBetaMaxValue game, (game.result state, action), limit - 1, -Infinity, +Infinity
.argmin (x) -> x.outcome

alphaBetaMaxValue = (game, state, limit, alpha, beta) ->
if game.terminalTest state
game.utility state
if limit < 1
game.heuristic state
v = -Infinity
for action in game.actions state
v = Math.max v, (alphaBetaMinValue game, (game.result state, action), limit, alpha, beta)
if v >= beta
v
alpha = Math.max alpha, v
v

alphaBetaMinValue = (game, state, limit, alpha, beta) ->
if game.terminalTest state
game.utility state
if limit < 1
game.heuristic state
v = +Infinity
for action in game.actions state
v = Math.min v, (alphaBetaMaxValue game, (game.result state, action), limit, alpha, beta)
if v <= alpha
v
beta = Math.min beta, v
v
###

for algorithm in [minimaxDecision, alphaBetaSearch]
state = [
['x', 'o', 'x']
['o', 'x', 'x']
[' ', 'o', ' ']
]

# player 2
decision = algorithm ticTacToe, state
state = ticTacToe.result state, decision.action
assert.deepEqual state, [
['x', 'o', 'x']
['o', 'x', 'x']
['o', 'o', ' ']
]
assert not ticTacToe.terminalTest state
assert.equal ticTacToe.utility(state), undefined

# player 1
decision = algorithm ticTacToe, state
state = ticTacToe.result state, decision.action
assert.deepEqual state, [
['x', 'o', 'x']
['o', 'x', 'x']
['o', 'o', 'x']
]
assert ticTacToe.terminalTest state
assert.equal ticTacToe.utility(state), 1

## Constraint Satisfaction

Confer section 6.1, p. 202.

export class ConstraintSatisfactionProblem extends SearchProblem
constructor: ({ domains, constraints }) ->
get = (pairList, key) ->
pairList.find(([key2, value]) -> key == key2)[1]
super
initialState: []
actions: (state) ->
if state.length < domains.length
domains[state.length][1]
.map (value) ->
[domains[state.length][0], value]
else
[]
result: (state, action) -> [...state, action]
stepCost: (state) -> 0
goalTest: (state) ->
state.length == domains.length and
(domains.every ([varName, domain]) ->
domain.some (v) ->
deepEqual v, (get state, varName)) and
constraints.every ([varList, constraint]) ->
varList.every (vars) ->
constraint \
...vars.map (varName) ->
get state, varName
@domains = domains
@constraints = constraints

variables: ->
@domains.keys()

satisfied: (solution) ->
@goalTest solution

#### Map Coloring Problem

Confer section 6.1.1, p. 203.

export mapColoringProblem = new ConstraintSatisfactionProblem
domains: ['WA', 'NT', 'Q', 'NSW', 'V', 'SA', 'T'] \
.map (state) -> [state, ['red', 'green', 'blue'] ]
constraints: [
[
[
['SA' , 'WA' ]
['SA' , 'NT' ]
['SA' , 'Q' ]
['SA' , 'NSW']
['SA' , 'V' ]
['WA' , 'NT' ]
['NT' , 'Q' ]
['Q' , 'NSW']
['NSW', 'V' ]
]
(a, b) -> a != b
]
]
###

state = mapColoringProblem.initialState
assert.deepEqual state, []
assert not mapColoringProblem.satisfied state
assert.deepEqual (mapColoringProblem.actions state), [['WA', 'red'], ['WA', 'green'], ['WA', 'blue']]

state = mapColoringProblem.result state, ['WA', 'red']
assert.deepEqual state, [['WA', 'red']]
assert not mapColoringProblem.satisfied state
assert.deepEqual (mapColoringProblem.actions state), [['NT', 'red'], ['NT', 'green'], ['NT', 'blue']]

state = mapColoringProblem.result state, ['NT', 'green']
assert.deepEqual state, [['WA', 'red'], ['NT', 'green']]
assert not mapColoringProblem.satisfied state
assert.deepEqual (mapColoringProblem.actions state), [['Q', 'red'], ['Q', 'green'], ['Q', 'blue']]

solution = [
['WA' , 'blue' ]
['NT' , 'green']
['Q' , 'red' ]
['NSW', 'green']
['V' , 'red' ]
['SA' , 'blue' ]
['T' , 'red' ]
]
assert not mapColoringProblem.satisfied solution

solution = [
['WA' , 'red' ]
['NT' , 'green']
['Q' , 'red' ]
['NSW', 'green']
['V' , 'red' ]
['SA' , 'green' ]
['T' , 'red' ]
]
assert not mapColoringProblem.satisfied solution

solution = [
['WA' , 'red' ]
['NT' , 'green']
['Q' , 'red' ]
['NSW', 'green']
['V' , 'blue' ]
['SA' , 'blue' ]
['T' , 'red' ]
]
assert not mapColoringProblem.satisfied solution

solution = [
['WA' , 'red' ]
['NT' , 'green']
['Q' , 'red' ]
['NSW', 'green']
['V' , 'red' ]
['SA' , 'blue' ]
['T' , 'red' ]
]
assert mapColoringProblem.satisfied solution

assert.deepEqual (depthFirstSearch mapColoringProblem).state, [
['WA' , 'blue' ]
['NT' , 'green']
['Q' , 'blue' ]
['NSW', 'green']
['V' , 'blue' ]
['SA' , 'red' ]
['T' , 'blue' ]
]

#### Constraint-Satisfaction 8-Queens Problem

Constraint-satisfaction formulation of the eight-queens problem.
Confer section 6.1.3, p. 205.

export constraintSatisfactionEightQueensProblem = new ConstraintSatisfactionProblem
domains: [0, 1, 2, 3, 4, 5, 6, 7].map (queen) -> [
queen
[0, 1, 2, 3, 4, 5, 6, 7].flatMap (y) ->
[0, 1, 2, 3, 4, 5, 6, 7].map (x) ->
[y, x]
]
constraints: [
[
[0, 1, 2, 3, 4, 5, 6, 7].flatMap (queen1) ->
[0, 1, 2, 3, 4, 5, 6, 7]
.filter (queen2) -> queen1 != queen2
.map (queen2) -> [queen1, queen2]
(a, b) ->
attacks = ([y1, x1], [y2, x2]) ->
y1 == y2 or
x1 == x2 or
y2 - y1 == x2 - x1
not 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 (row, y) -> [y, [y, row.findIndex (x) -> x == 1] ]
assert not constraintSatisfactionEightQueensProblem.satisfied solution

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 (row, y) -> [y, [y, row.findIndex (x) -> x == 1] ]
assert constraintSatisfactionEightQueensProblem.satisfied solution

#### Node Consistency

Confer section 6.2.1, p. 208.

export makeNodeConsistent = (problem) ->
new ConstraintSatisfactionProblem
domains: problem.domains.map ([varName, domain]) ->
[
varName,
domain.filter (value) ->
applicableUnaryConstraints problem.constraints, varName
.every ([varList, constraintFunc]) ->
constraintFunc value
]
constraints: nonUnaryConstraints problem.constraints

applicableUnaryConstraints = (constraints, varName) ->
unaryConstraints constraints
.filter (constraint) ->
constraint[0].some (varName2) ->
varName2 == varName

unaryConstraints = (constraints) ->
constraints.filter (constraint) -> constraint[1].length == 1

nonUnaryConstraints = (constraints) ->
constraints.filter (constraint) -> constraint[1].length > 1
###

problem = new ConstraintSatisfactionProblem
domains: [
['Northern Australia', ['red', 'blue', 'green'] ]
['Southern Australia', ['red', 'blue', 'green'] ]
]
constraints: [
[['Northern Australia', 'Southern Australia'], (a, b) -> a != b]
[['Southern Australia'], (a) -> a != 'green']
]

problem = makeNodeConsistent problem
assert.deepEqual problem.domains, [
['Northern Australia', ['red', 'blue', 'green']]
['Southern Australia', ['red', 'blue']]
]

## 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 plParse = (sentence, vars) ->
recursiveParse sentence.split(''), vars

unaryOperators = ['~']
binaryOperators = ['&', '|', '>', '=']
operators = [...unaryOperators, ...binaryOperators]

recursiveParse = (sentence, vars) ->
if (levels sentence)[sentence.length - 1] == 0 and
sentence[0] == '(' and
sentence[sentence.length - 1] == ')'
if (levels sentence).some (level) -> level > 1
if (operatorIndex sentence) > -1
[
(sentence.slice \
(operatorIndex sentence), \
(operatorIndex sentence) + 1 \
)[0],
...if not unaryOperators.includes sentence[operatorIndex sentence]
[recursiveParse (sentence.slice 1, operatorIndex sentence), vars]
else
[]
recursiveParse (sentence.slice (operatorIndex sentence) + 1, -1)
]
else
recursiveParse sentence.slice 1, -1
else if operators.some (operator) -> sentence.includes operator
'ERROR'
else if sentence.length == 3 and sentence[1] == 'T'
true
else if sentence.length == 3 and sentence[1] == 'F'
false
else if typeof vars == 'undefined' or
vars.includes (sentence.slice 1, -1).join ''
(sentence.slice 1, -1).join ''
else
'UNDEFINED'
else
'ERROR'

levels = (sentence) ->
sentence
.map (char) ->
if char == '('
1
else if char == ')'
-1
else
0
.reduce (prev, derivation) ->
[
...prev,
prev[prev.length - 1] + derivation
]
, [0]
.slice(1)

operatorIndex = (sentence) ->
sentence.findIndex (char, i) ->
(levels sentence)[i] == 1 and operators.includes char
###

for [proposition, syntax] in [
[ [ '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' ]
]
assert.deepEqual (plParse ...proposition), syntax

## Learning

### Models

export class Hypothesis
constructor: (@weights) ->

#### Linear Model

export class LinearModel extends Hypothesis
apply: (x) ->
if x.length == (@weights.length - 1)
@weights[0] +
@weights
.slice 1
.sum (w, i) ->
w *
x[i]

complexity: (q = 1) ->
@weights.sum (w) ->
Math.abs(w) ** q

#### Polynomial

export class Polynomial extends Hypothesis
apply: (x) ->
@weights
.sum (w, i) ->
w * (x ** i)

For example, f(x) = 2x² + 5x + 3:

f = new Polynomial([3, 5, 2])
assert.deepEqual \
([0, 1, 2, 3, 4, 5].map (x) -> f.apply x), \
[3, 10, 21, 36, 55, 78]

### Evaluation

#### Empirical Loss

Confer section 18.4.2, p. 712f.

export lossFunctions =
absolute: (correct, predicted) -> Math.abs (correct - predicted),
squared: (correct, predicted) -> (correct - predicted) ** 2,
binary: (correct, predicted) -> if correct == predicted then 1 else 0

export empiricalLoss = (hypothesis, lossFunction, examples) ->
(
examples.sum (pair) ->
lossFunction pair[1], hypothesis.apply [pair[0]]
) /
examples.length

#### Cost & Best Hypothesis

Confer section 18.4.3, p. 713.

export cost = (hypothesis, lossFunction, examples, conversionRate = 0) ->
(empiricalLoss hypothesis, lossFunction, examples) +
conversionRate *
hypothesis.complexity

export bestHypothesis = \
(hypothesisSpace, lossFunction, examples, conversionRate = 0) ->
hypothesisSpace.argmin (x) -> cost x, lossFunction, examples, conversionRate

### Regression

#### Univariate Linear Regression

Confer section 18.6.1, p. 719

export linearRegression = (trainingSet) ->
divisor = \
trainingSet.length *
( trainingSet.sum (pair) -> pair[0] ** 2) -
((trainingSet.sum (pair) -> pair[0]) ** 2)
w1 =
if divisor == 0
0
else
(
trainingSet.length *
(trainingSet.sum (pair) -> pair[0] * pair[1]) -
(trainingSet.sum (pair) -> pair[0]) *
(trainingSet.sum (pair) -> pair[1])
) / divisor
w0 = \
(
(trainingSet.sum (pair) -> pair[1]) -
w1 *
(trainingSet.sum (pair) => pair[0])
) /
trainingSet.length
new LinearModel [w0, w1]
###

trueModel = new LinearModel [3, 0.5]
sampleSizes = [1, 5, 10, 100]
assert.deepEqual (
sampleSizes.map (n) ->
examples = [0 .. (n - 1)].map (x) ->
[x, (trueModel.apply [x]) + rand.random() - 0.5]
predictedModel = linearRegression examples
[n, predictedModel.weights, empiricalLoss predictedModel, lossFunctions.squared, examples]
), [
# n w0 w1 empirical loss
[ 1 , [ 2.9863217363830663 , 0 ], 0 ]
[ 5 , [ 2.933798302215137 , 0.4203891319253779 ], 0.026432483146730707 ]
[ 10 , [ 3.1695157940114393 , 0.4639499877188505 ], 0.07533178680729899 ]
[ 100 , [ 3.0419489353360176 , 0.4986560392449387 ], 0.08407918383200677 ]
]

### Artificial Neural Networks

#### Activation Function

Confer section 18.7.1, p. 729.

export activationFunctions =
step: (x) -> (x > 0) * 1
sigmoid: (x) -> 1 / (1 + Math.E ** (-x))

#### Neuron

Confer section 18.7.1, p. 728.

export class Neuron
constructor: ({
@inputs = []
@threshold = 0
@activationFunction = activationFunctions.sigmoid
weights
}) ->
@weights = weights || (Array @inputs.length).fill 1

output: ->
@activationFunction \
(
@inputs
.map (input, i) =>
@weights[i] *
@inputs[i].output()
.sum()
) -
@threshold

Neurons as logic gates. Confer section 18.7.2, p. 729f.

inputs = [{}, {}]
AND = new Neuron
inputs: inputs
weights: [1, 1]
threshold: 1.5
activationFunction: activationFunctions.step
OR = new Neuron
inputs: inputs
weights: [1, 1]
threshold: 0.5
activationFunction: activationFunctions.step
NOT = new Neuron
inputs: [inputs[0]]
weights: [-1]
threshold: -0.5
activationFunction: activationFunctions.step
###

inputs[0].output = -> 0
inputs[1].output = -> 0
assert.equal AND.output(), 0
assert.equal OR.output(), 0
assert.equal NOT.output(), 1

inputs[0].output = -> 0
inputs[1].output = -> 1
assert.equal AND.output(), 0
assert.equal OR.output(), 1
assert.equal NOT.output(), 1

inputs[0].output = -> 1
inputs[1].output = -> 0
assert.equal AND.output(), 0
assert.equal OR.output(), 1
assert.equal NOT.output(), 0

inputs[0].output = -> 1
inputs[1].output = -> 1
assert.equal AND.output(), 1
assert.equal OR.output(), 1
assert.equal NOT.output(), 0

Neurons representing the majority / minority function. Confer section 18.7.2, p. 731.

export majority = (inputs) -> new Neuron
inputs: inputs
weights: (Array inputs.length).fill 1
threshold: inputs.length / 2
activationFunction: activationFunctions.step

export minority = (inputs) -> new Neuron
inputs: inputs
weights: (Array inputs.length).fill -1
threshold: -inputs.length / 2
activationFunction: activationFunctions.step
###

inputs = [0, 0, 0, undefined, 1, 1, 1]
.map (i) -> ({ output: -> i })

sevenMajority = majority inputs
sevenMinority = minority inputs

inputs[3].output = -> 0
assert.equal sevenMajority.output(), 0
assert.equal sevenMinority.output(), 1

inputs[3].output = -> 1
assert.equal sevenMajority.output(), 1
assert.equal sevenMinority.output(), 0