├── paip
├── __init__.py
├── tests
│ ├── __init__.py
│ ├── test_eliza.py
│ ├── test_gps.py
│ ├── test_search.py
│ ├── test_othello.py
│ ├── test_logic.py
│ └── test_emycin.py
├── examples
│ ├── __init__.py
│ ├── eliza
│ │ ├── __init__.py
│ │ ├── support.py
│ │ └── eliza.py
│ ├── emycin
│ │ ├── __init__.py
│ │ └── mycin.py
│ ├── gps
│ │ ├── __init__.py
│ │ ├── school.py
│ │ ├── monkeys.py
│ │ └── blocks.py
│ ├── logic
│ │ ├── __init__.py
│ │ ├── find_list.py
│ │ ├── find_length.py
│ │ ├── find_list_length_4.py
│ │ ├── find_elements.py
│ │ ├── transitive.py
│ │ ├── likes.py
│ │ └── find_lists_lengths.py
│ ├── othello
│ │ ├── __init__.py
│ │ └── othello.py
│ ├── search
│ │ ├── __init__.py
│ │ ├── pathfinding.py
│ │ └── gps.py
│ └── prolog
│ │ ├── transitive.prolog
│ │ ├── pair.prolog
│ │ ├── graph.prolog
│ │ └── family.prolog
├── abandoned
│ ├── generate_blocks.py
│ ├── pattern.py
│ └── logic.lisp
├── sentences.py
├── gps.py
├── eliza.py
├── search.py
└── othello.py
├── .gitignore
├── run_tests.py
├── LICENSE
├── run_examples.py
├── README.md
└── prolog.py
/paip/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/paip/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/paip/examples/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/paip/examples/eliza/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/paip/examples/emycin/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/paip/examples/gps/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/paip/examples/logic/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/paip/examples/othello/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/paip/examples/search/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | .DS_Store
3 | *.pyc
4 | *.sublime*
5 |
--------------------------------------------------------------------------------
/paip/examples/prolog/transitive.prolog:
--------------------------------------------------------------------------------
1 | # example of transitive relation
2 |
3 | <- likes(joe, judy)
4 | <- likes(judy, jorge)
5 | <- likes(?x, ?x)
6 | <- likes(?x, ?y) :- likes(?x, ?z), likes(?z, ?y)
7 |
--------------------------------------------------------------------------------
/paip/examples/prolog/pair.prolog:
--------------------------------------------------------------------------------
1 | # defining lists in prolog
2 |
3 | <- first(?x, pair(?x, ?more))
4 | <- rest(?more, pair(?x, ?more))
5 |
6 | <- member(?x, pair(?x, ?more))
7 | <- member(?x, pair(?y, ?more)) :- member(?x, ?more)
8 |
9 | <- length(nil, 0)
10 | <- length(pair(?x, nil), inc(0))
11 | <- length(pair(?first, ?rest), inc(?n)) :- length(?rest, ?n)
12 |
--------------------------------------------------------------------------------
/paip/examples/prolog/graph.prolog:
--------------------------------------------------------------------------------
1 | # construct the graph
2 |
3 | # a---b---e
4 | # \ / /
5 | # c---d
6 |
7 | <- linked(a, b)
8 | <- linked(b, c)
9 | <- linked(a, c)
10 | <- linked(c, d)
11 | <- linked(d, e)
12 | <- linked(b, e)
13 |
14 | # node ?y is reachable from node ?x if there exists a path from ?x to ?y
15 | <- reachable(?x, ?y) :- linked(?x, ?z), linked(?z, ?y)
16 | <- reachable(?x, ?y) :- linked(?x, ?y)
17 | <- reachable(?x, ?y) :- linked(?y, ?x) # undirected graph
18 |
19 |
--------------------------------------------------------------------------------
/run_tests.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | import unittest
4 |
5 | tests_dir = 'paip/tests'
6 |
7 | for file in os.listdir(tests_dir):
8 | # Uncomment to enable test logging:
9 | #logging.basicConfig(level=logging.DEBUG)
10 |
11 | if not file.startswith('test_') or not file.endswith('.py'):
12 | continue
13 | qual_file = os.path.join(tests_dir, file)
14 | module = qual_file.replace('/', '.')[:-3] # leave off .py
15 | print 'Testing module %s' % module
16 | unittest.main(module, exit=False)
17 |
--------------------------------------------------------------------------------
/paip/examples/logic/find_list.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from paip import logic
3 |
4 | def main():
5 | x = logic.Var('x')
6 | y = logic.Var('y')
7 | a = logic.Var('a')
8 | more = logic.Var('more')
9 |
10 | member_first = logic.Clause(
11 | logic.Relation('member', (x, logic.Relation('pair', (x, more)))))
12 |
13 | member_last = logic.Clause(
14 | logic.Relation('member', (x, logic.Relation('pair', (y, x)))))
15 |
16 | member_rest = logic.Clause(
17 | logic.Relation('member', (x, logic.Relation('pair', (y, more)))),
18 | [logic.Relation('member', (x, more))])
19 |
20 | db = {}
21 | logic.store(db, member_first)
22 | logic.store(db, member_last)
23 | logic.store(db, member_rest)
24 |
25 | print 'Database:'
26 | print db
27 | print
28 |
29 | query = logic.Relation('member', (logic.Atom('foo'), x))
30 | print 'Query:', query
31 | print
32 |
33 | logic.prolog_prove([query], db)
34 |
--------------------------------------------------------------------------------
/paip/abandoned/generate_blocks.py:
--------------------------------------------------------------------------------
1 | import json
2 | import sys
3 |
4 | def move_op(a, b, c):
5 | return {
6 | 'action': 'move %s from %s to %s' % (a, b, c),
7 | 'preconds': [
8 | 'space on %s' % a,
9 | 'space on %s' % c,
10 | '%s on %s' % (a, b)
11 | ],
12 | 'add': move_ons(a, b, c),
13 | 'delete': move_ons(a, c, b),
14 | }
15 |
16 |
17 | def move_ons(a, b, c):
18 | moves = ['%s on %s' % (a, c)]
19 | if b != 'table':
20 | moves.append('space on %s' % b)
21 | return moves
22 |
23 |
24 | def generate(blocks):
25 | ops = []
26 | for a in blocks:
27 | for b in blocks:
28 | if a == b: continue
29 | for c in blocks:
30 | if c in (a, b): continue
31 | ops.append(move_op(a, b, c))
32 | ops.append(move_op(a, 'table', b))
33 | ops.append(move_op(a, b, 'table'))
34 | print json.dumps(ops, indent=4)
35 |
36 |
37 | if __name__ == '__main__':
38 | generate(sys.argv[1:])
39 |
--------------------------------------------------------------------------------
/paip/examples/logic/find_length.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from paip import logic
3 |
4 | def main():
5 | x = logic.Var('x')
6 | y = logic.Var('y')
7 | z = logic.Var('z')
8 | a = logic.Var('a')
9 | nil = logic.Atom('nil')
10 | more = logic.Var('more')
11 | zero = logic.Atom('0')
12 |
13 | length_nil = logic.Clause(logic.Relation('length', (nil, zero)))
14 | length_one = logic.Clause(
15 | logic.Relation('length',
16 | (logic.Relation('pair', (x, more)),
17 | logic.Relation('+1', [a]))),
18 | [logic.Relation('length', (more, a))])
19 |
20 | db = {}
21 | logic.store(db, length_nil)
22 | logic.store(db, length_one)
23 |
24 | print 'Database:'
25 | print db
26 | print
27 |
28 | list = logic.Relation(
29 | 'pair', (x, logic.Relation(
30 | 'pair', (y, logic.Relation(
31 | 'pair', (z, nil))))))
32 |
33 | query = logic.Relation('length', (list, a))
34 | print 'Query:', query
35 | print
36 |
37 | logic.prolog_prove([query], db)
38 |
--------------------------------------------------------------------------------
/paip/examples/prolog/family.prolog:
--------------------------------------------------------------------------------
1 | # a family tree example
2 | # from http://www.cs.toronto.edu/~hojjat/384f06/simple-prolog-examples.html
3 | # for some reason, in this family tree, reproduction is asexual.
4 |
5 | <- male(james1)
6 | <- male(charles1)
7 | <- male(charles2)
8 | <- male(james2)
9 | <- male(george1)
10 |
11 | <- female(catherine)
12 | <- female(elizabeth)
13 | <- female(sophia)
14 |
15 | # parent(?x, ?y) means ?y is the parent of ?x
16 | <- parent(charles1, james1)
17 | <- parent(elizabeth, james1)
18 | <- parent(charles2, charles1)
19 | <- parent(catherine, charles1)
20 | <- parent(james2, charles1)
21 | <- parent(sophia, elizabeth)
22 | <- parent(george1, sophia)
23 |
24 | <- mother(?x, ?m) :- parent(?x, ?m), female(?m)
25 | <- father(?x, ?f) :- parent(?x, ?f), male(?f)
26 | <- sibling(?x, ?y) :- parent(?x, ?p), parent(?y, ?p)
27 |
28 | <- sister(?x, ?y) :- sibling(?x, ?y), female(?y)
29 | <- brother(?x, ?y) :- sibling(?x, ?y), male(?y)
30 | <- grandparent(?x, ?y) :- parent(?x, ?z), parent(?z, ?y)
31 |
32 | <- ancestor(?x, ?y) :- parent(?x, ?y)
33 | <- ancestor(?x, ?y) :- ancestor(?x, ?z), ancestor(?z, ?y)
34 |
--------------------------------------------------------------------------------
/paip/examples/logic/find_list_length_4.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from paip import logic
3 |
4 | def main():
5 | x = logic.Var('x')
6 | y = logic.Var('y')
7 | z = logic.Var('z')
8 | a = logic.Var('a')
9 | nil = logic.Atom('nil')
10 | more = logic.Var('more')
11 | zero = logic.Atom('0')
12 |
13 | length_nil = logic.Clause(logic.Relation('length', (nil, zero)))
14 | length_one = logic.Clause(
15 | logic.Relation('length',
16 | (logic.Relation('pair', (x, more)),
17 | logic.Relation('+1', [a]))),
18 | [logic.Relation('length', (more, a))])
19 |
20 | db = {}
21 | logic.store(db, length_nil)
22 | logic.store(db, length_one)
23 |
24 | print 'Database:'
25 | print db
26 | print
27 |
28 | four = logic.Relation(
29 | '+1', [logic.Relation(
30 | '+1', [logic.Relation(
31 | '+1', [logic.Relation('+1', [zero])])])])
32 |
33 | query = logic.Relation('length', (x, four))
34 | print 'Query:', query
35 | print
36 |
37 | logic.prolog_prove([query], db)
38 |
--------------------------------------------------------------------------------
/paip/examples/logic/find_elements.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from paip import logic
3 |
4 | def main():
5 | x = logic.Var('x')
6 | y = logic.Var('y')
7 | a = logic.Var('a')
8 | nil = logic.Atom('nil')
9 | more = logic.Var('more')
10 |
11 | member_first = logic.Clause(
12 | logic.Relation('member', (x, logic.Relation('pair', (x, more)))))
13 |
14 | member_last = logic.Clause(
15 | logic.Relation('member', (x, logic.Relation('pair', (y, x)))))
16 |
17 | member_rest = logic.Clause(
18 | logic.Relation('member', (x, logic.Relation('pair', (y, more)))),
19 | [logic.Relation('member', (x, more))])
20 |
21 | db = {}
22 | logic.store(db, member_first)
23 | logic.store(db, member_last)
24 | logic.store(db, member_rest)
25 |
26 | list = logic.Relation(
27 | 'pair', (logic.Atom('foo'), logic.Relation(
28 | 'pair', (logic.Atom('bar'), logic.Relation(
29 | 'pair', (logic.Atom('baz'), nil))))))
30 |
31 | print 'Database:'
32 | print db
33 | print
34 |
35 | query = logic.Relation('member', (x, list))
36 | print 'Query:', query
37 | print
38 |
39 | logic.prolog_prove([query], db)
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015, Daniel Connelly
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | * Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 |
--------------------------------------------------------------------------------
/paip/examples/logic/transitive.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from paip import logic
3 |
4 | def main():
5 | #logging.basicConfig(level=logging.DEBUG)
6 | db = {}
7 |
8 | kim = logic.Atom('Kim')
9 | robin = logic.Atom('Robin')
10 | sandy = logic.Atom('Sandy')
11 | lee = logic.Atom('Lee')
12 | cats = logic.Atom('cats')
13 | x = logic.Var('x')
14 | y = logic.Var('y')
15 | z = logic.Var('z')
16 |
17 | self_likes = logic.Clause(logic.Relation('likes', (x, x)))
18 | transitive_likes = logic.Clause(logic.Relation('likes', (x, y)),
19 | (logic.Relation('likes', (x, z)), logic.Relation('likes', (z, y))))
20 |
21 | klr = logic.Clause(logic.Relation('likes', (kim, robin)))
22 | sll = logic.Clause(logic.Relation('likes', (sandy, lee)))
23 | slk = logic.Clause(logic.Relation('likes', (sandy, kim)))
24 | rlc = logic.Clause(logic.Relation('likes', (robin, cats)))
25 | llr = logic.Clause(logic.Relation('likes', (lee, robin)))
26 |
27 | logic.store(db, klr)
28 | logic.store(db, sll)
29 | logic.store(db, slk)
30 | logic.store(db, rlc)
31 | logic.store(db, llr)
32 |
33 | logic.store(db, self_likes)
34 | logic.store(db, transitive_likes)
35 |
36 | print 'Database:'
37 | print db
38 | print
39 |
40 | query = logic.Relation('likes', (sandy, logic.Var('who')))
41 | print 'Query:', str(query)
42 | print
43 |
44 | logic.prolog_prove([query], db)
45 |
46 |
--------------------------------------------------------------------------------
/paip/examples/gps/school.py:
--------------------------------------------------------------------------------
1 | from paip.gps import gps
2 |
3 | problem = {
4 | "start": ["son at home", "have money", "have phone book", "car needs battery"],
5 | "finish": ["son at school"],
6 | "ops": [
7 | {
8 | "action": "drive son to school",
9 | "preconds": ["son at home", "car works"],
10 | "add": ["son at school"],
11 | "delete": ["son at home"]
12 | },
13 | {
14 | "action": "shop installs battery",
15 | "preconds": ["car needs battery", "shop knows problem", "shop has money"],
16 | "add": ["car works"],
17 | "delete": []
18 | },
19 | {
20 | "action": "tell shop problem",
21 | "preconds": ["in communication with shop"],
22 | "add": ["shop knows problem"],
23 | "delete": []
24 | },
25 | {
26 | "action": "telephone shop",
27 | "preconds": ["know phone number"],
28 | "add": ["in communication with shop"],
29 | "delete": []
30 | },
31 | {
32 | "action": "look up number",
33 | "preconds": ["have phone book"],
34 | "add": ["know phone number"],
35 | "delete": []
36 | },
37 | {
38 | "action": "give shop money",
39 | "preconds": ["have money"],
40 | "add": ["shop has money"],
41 | "delete": ["have money"]
42 | }
43 | ]
44 | }
45 |
46 | def main():
47 | start = problem['start']
48 | finish = problem['finish']
49 | ops = problem['ops']
50 | for action in gps(start, finish, ops):
51 | print action
52 |
53 | if __name__ == '__main__':
54 | main()
55 |
--------------------------------------------------------------------------------
/paip/examples/logic/likes.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from paip import logic
3 |
4 | def main():
5 | #logging.basicConfig(level=logging.DEBUG)
6 | db = {}
7 |
8 | kim = logic.Atom('Kim')
9 | robin = logic.Atom('Robin')
10 | sandy = logic.Atom('Sandy')
11 | lee = logic.Atom('Lee')
12 | cats = logic.Atom('cats')
13 | x = logic.Var('x')
14 |
15 | sandy_likes = logic.Relation('likes', (sandy, x))
16 | likes_cats = logic.Relation('likes', (x, cats))
17 | sandy_likes_rule = logic.Clause(sandy_likes, [likes_cats])
18 |
19 | kim_likes = logic.Relation('likes', (kim, x))
20 | likes_lee = logic.Relation('likes', (x, lee))
21 | likes_kim = logic.Relation('likes', (x, kim))
22 | kim_likes_rule = logic.Clause(kim_likes, [likes_lee, likes_kim])
23 |
24 | likes_self = logic.Clause(logic.Relation('likes', (x, x)))
25 | klr = logic.Clause(logic.Relation('likes', (kim, robin)))
26 | sll = logic.Clause(logic.Relation('likes', (sandy, lee)))
27 | slk = logic.Clause(logic.Relation('likes', (sandy, kim)))
28 | rlc = logic.Clause(logic.Relation('likes', (robin, cats)))
29 |
30 | logic.store(db, sandy_likes_rule)
31 | logic.store(db, kim_likes_rule)
32 | logic.store(db, likes_self)
33 | logic.store(db, klr)
34 | logic.store(db, sll)
35 | logic.store(db, slk)
36 | logic.store(db, rlc)
37 |
38 | print 'Database:'
39 | print db
40 | print
41 |
42 | query = logic.Relation('likes', (sandy, logic.Var('who')))
43 | print 'Query:', str(query)
44 | print
45 |
46 | logic.prolog_prove([query], db)
47 |
48 |
--------------------------------------------------------------------------------
/paip/examples/gps/monkeys.py:
--------------------------------------------------------------------------------
1 | from paip.gps import gps
2 |
3 | problem = {
4 | "start": ["at door", "on floor", "has ball", "hungry", "chair at door"],
5 | "finish": ["not hungry"],
6 | "ops": [
7 | {
8 | "action": "climb on chair",
9 | "preconds": ["chair at middle room", "at middle room", "on floor"],
10 | "add": ["at bananas", "on chair"],
11 | "delete": ["at middle room", "on floor"]
12 | },
13 | {
14 | "action": "push chair from door to middle room",
15 | "preconds": ["chair at door", "at door"],
16 | "add": ["chair at middle room", "at middle room"],
17 | "delete": ["chair at door", "at door"]
18 | },
19 | {
20 | "action": "walk from door to middle room",
21 | "preconds": ["at door", "on floor"],
22 | "add": ["at middle room"],
23 | "delete": ["at door"]
24 | },
25 | {
26 | "action": "grasp bananas",
27 | "preconds": ["at bananas", "empty handed"],
28 | "add": ["has bananas"],
29 | "delete": ["empty handed"]
30 | },
31 | {
32 | "action": "drop ball",
33 | "preconds": ["has ball"],
34 | "add": ["empty handed"],
35 | "delete": ["has ball"]
36 | },
37 | {
38 | "action": "eat bananas",
39 | "preconds": ["has bananas"],
40 | "add": ["empty handed", "not hungry"],
41 | "delete": ["has bananas", "hungry"]
42 | }
43 | ]
44 | }
45 |
46 | def main():
47 | start = problem['start']
48 | finish = problem['finish']
49 | ops = problem['ops']
50 | for action in gps(start, finish, ops):
51 | print action
52 |
53 | if __name__ == '__main__':
54 | main()
55 |
--------------------------------------------------------------------------------
/run_examples.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import logging
3 | import os
4 | import sys
5 |
6 |
7 | parser = argparse.ArgumentParser(description='Run example AI programs.')
8 | parser.add_argument('--log', action='store_true', help='Turn on logging')
9 |
10 |
11 | def main():
12 | print 'Please choose an example to run:'
13 | modules = []
14 | for i, name in enumerate(discover_modules('paip/examples')):
15 | __import__(name)
16 | module = sys.modules[name]
17 | modules.append(module)
18 | print '%d\t%s' % (i, module.__name__)
19 | while True:
20 | try:
21 | choice = raw_input('>> ')
22 | if not choice:
23 | continue
24 | ind = int(choice)
25 | except ValueError:
26 | print 'That is not a valid option. Please try again.'
27 | continue
28 | except EOFError:
29 | print 'Goodbye.'
30 | return
31 | try:
32 | module = modules[ind]
33 | except IndexError:
34 | print 'That is not a valid option. Please try again.'
35 | else:
36 | print module.__doc__
37 | return module.main()
38 |
39 |
40 | def discover_modules(root):
41 | modules = []
42 | for root, dirs, files in os.walk(root):
43 | for f in (f for f in files if f.endswith('.py') and '__init__' not in f):
44 | path = os.path.join(root, f)[:-3]
45 | if root.startswith('.'):
46 | path = path[2:]
47 | modules.append(path.replace(os.sep, '.'))
48 | return modules
49 |
50 |
51 | if __name__ == '__main__':
52 | args = vars(parser.parse_args())
53 | if args['log']:
54 | logging.basicConfig(level=logging.DEBUG)
55 | main()
56 |
--------------------------------------------------------------------------------
/paip/examples/logic/find_lists_lengths.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from paip import logic
3 |
4 | def main():
5 | x = logic.Var('x')
6 | y = logic.Var('y')
7 | z = logic.Var('z')
8 | a = logic.Var('a')
9 | nil = logic.Atom('nil')
10 | more = logic.Var('more')
11 | zero = logic.Atom('0')
12 |
13 | length_nil = logic.Clause(logic.Relation('length', (nil, zero)))
14 | length_one = logic.Clause(
15 | logic.Relation('length',
16 | (logic.Relation('pair', (x, more)),
17 | logic.Relation('+1', [a]))),
18 | [logic.Relation('length', (more, a))])
19 |
20 | member_first = logic.Clause(
21 | logic.Relation('member', (x, logic.Relation('pair', (x, more)))))
22 |
23 | member_last = logic.Clause(
24 | logic.Relation('member', (x, logic.Relation('pair', (y, x)))))
25 |
26 | member_end = logic.Clause(
27 | logic.Relation('member', (x, logic.Relation('pair', (x, nil)))))
28 |
29 | member_rest = logic.Clause(
30 | logic.Relation('member', (x, logic.Relation('pair', (y, more)))),
31 | [logic.Relation('member', (x, more))])
32 |
33 | db = {}
34 | logic.store(db, length_nil)
35 | logic.store(db, length_one)
36 | logic.store(db, member_end)
37 | logic.store(db, member_first)
38 | logic.store(db, member_last)
39 | logic.store(db, member_rest)
40 |
41 | print 'Database:'
42 | print db
43 | print
44 |
45 | four = logic.Relation(
46 | '+1', [logic.Relation(
47 | '+1', [logic.Relation(
48 | '+1', [logic.Relation('+1', [zero])])])])
49 |
50 | foo = logic.Atom('foo')
51 |
52 | has_foo = logic.Relation('member', (foo, x))
53 | length_4 = logic.Relation('length', (x, a))
54 |
55 | print 'Query:', has_foo, length_4
56 | print
57 |
58 | logic.prolog_prove([has_foo, length_4], db)
59 |
--------------------------------------------------------------------------------
/paip/examples/eliza/support.py:
--------------------------------------------------------------------------------
1 | import json
2 | import sys
3 | from paip import eliza
4 |
5 | rules = {
6 | "?*x hello ?*y": [
7 | "Hello, my name is SUPPORT. How can I help you today?",
8 | ],
9 | "?*x manager ?*y": [
10 | "Our manager is not available right now. How can I help you?"
11 | ],
12 | "?*x problem with ?*y": [
13 | "Have you tried turning it off and back on?",
14 | "When did you first observe ?y to be a problem?",
15 | "How long has ?y been a problem?",
16 | "That is not covered by the warranty.",
17 | ],
18 | "?*x trouble with ?*y": [
19 | ("I'm sorry, ?y is handled by another department.\n" +
20 | "Please wait while I transfer your call."),
21 | "What seems to be the problem?",
22 | ],
23 | "?*x how to ?*y": [
24 | "Please consult the manual for more details.",
25 | "I'm sorry, I do not have that information.",
26 | "Can you be more specific?",
27 | "?y will void your warranty."
28 | ],
29 | "?*x why ?*y": [
30 | "I'm sorry, I can't discuss that.",
31 | ],
32 | "?*x ago ?*y": [
33 | "I'm sorry, your warranty ended a week earlier.",
34 | ]
35 | }
36 |
37 | default_responses = [
38 | "I do not understand.",
39 | "Please elaborate.",
40 | "Please hold."
41 | ]
42 |
43 | def main():
44 | # We need the rules in a list containing elements of the following form:
45 | # `(input pattern, [output pattern 1, output pattern 2, ...]`
46 | rules_list = []
47 | for pattern, transforms in rules.items():
48 | # Remove the punctuation from the pattern to simplify matching.
49 | pattern = eliza.remove_punct(str(pattern.upper())) # kill unicode
50 | transforms = [str(t).upper() for t in transforms]
51 | rules_list.append((pattern, transforms))
52 | eliza.interact('SUPPORT> ', rules_list, map(str.upper, default_responses))
53 |
54 | if __name__ == '__main__':
55 | main(sys.argv[1:])
56 |
--------------------------------------------------------------------------------
/paip/examples/othello/othello.py:
--------------------------------------------------------------------------------
1 | from paip import othello
2 |
3 | def check(move, player, board):
4 | return othello.is_valid(move) and othello.is_legal(move, player, board)
5 |
6 | def human(player, board):
7 | print othello.print_board(board)
8 | print 'Your move?'
9 | while True:
10 | move = raw_input('> ')
11 | if move and check(int(move), player, board):
12 | return int(move)
13 | elif move:
14 | print 'Illegal move--try again.'
15 |
16 | def get_choice(prompt, options):
17 | print prompt
18 | print 'Options:', options.keys()
19 | while True:
20 | choice = raw_input('> ')
21 | if choice in options:
22 | return options[choice]
23 | elif choice:
24 | print 'Invalid choice.'
25 |
26 | def get_players():
27 | print 'Welcome to OTHELLO!'
28 | options = { 'human': human,
29 | 'random': othello.random_strategy,
30 | 'max-diff': othello.maximizer(othello.score),
31 | 'max-weighted-diff': othello.maximizer(othello.weighted_score),
32 | 'minimax-diff': othello.minimax_searcher(3, othello.score),
33 | 'minimax-weighted-diff':
34 | othello.minimax_searcher(3, othello.weighted_score),
35 | 'ab-diff': othello.alphabeta_searcher(3, othello.score),
36 | 'ab-weighted-diff':
37 | othello.alphabeta_searcher(3, othello.weighted_score) }
38 | black = get_choice('BLACK: choose a strategy', options)
39 | white = get_choice('WHITE: choose a strategy', options)
40 | return black, white
41 |
42 | def main():
43 | try:
44 | black, white = get_players()
45 | board, score = othello.play(black, white)
46 | except othello.IllegalMoveError as e:
47 | print e
48 | return
49 | except EOFError as e:
50 | print 'Goodbye.'
51 | return
52 | print 'Final score:', score
53 | print '%s wins!' % ('Black' if score > 0 else 'White')
54 | print othello.print_board(board)
55 |
--------------------------------------------------------------------------------
/paip/examples/search/pathfinding.py:
--------------------------------------------------------------------------------
1 | """
2 | An application of A* pathfinding: find the best path through a map. The map is
3 | represented as a grid, where 1 is an obstacle and 0 is an open space. Movement
4 | can be up, down, left, right, and diagonal in one step.
5 | """
6 |
7 | ## Pathfinding
8 |
9 | def find_path(map, begin, end):
10 | """Find the best path between the begin and end position in the map."""
11 | start_path = [search.Path(begin)]
12 | cost = lambda loc1, loc2: abs(loc1[0] - loc2[0]) + abs(loc1[1] - loc2[1])
13 | remaining = lambda loc: cost(loc, end)
14 | done = lambda loc: loc == end
15 |
16 | path = search.a_star(start_path, done, map_successors, cost, remaining)
17 | return path.collect()
18 |
19 |
20 | def map_successors(location):
21 | """Get the locations accessible from the current location."""
22 | row, col = location
23 | possible = [(row + dy, col + dx) for dy in (-1, 0, 1) for dx in (-1, 0, 1)]
24 | # out of all the neighbor locations, filter out the current one, any
25 | # locations outside the map border, and the locations containing obstacles.
26 | successors = [(row, col) for (row, col) in possible
27 | if 0 <= row < len(MAP) and 0 <= col < len(MAP[0])
28 | and MAP[row][col] == 0
29 | and (row, col) != location]
30 | return successors
31 |
32 |
33 | ## Utilities
34 |
35 | def print_map(map):
36 | """Pretty-prints the given map to standard output."""
37 | print '-' * (2 * len(map) + 3)
38 | for row in map:
39 | print '|',
40 | for col in row:
41 | print '%s' % (col if col == 1 or col == 'X' else ' '),
42 | print '|'
43 | print '-' * (2 * len(map) + 3)
44 |
45 |
46 | ## Running from the command line
47 |
48 | from paip import search
49 |
50 |
51 | MAP = [
52 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
53 | [0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
54 | [0, 1, 1, 0, 1, 1, 0, 0, 1, 0],
55 | [0, 0, 1, 0, 1, 1, 0, 1, 1, 0],
56 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
57 | [0, 1, 1, 1, 0, 1, 1, 0, 0, 0],
58 | [0, 0, 0, 0, 0, 1, 1, 0, 0, 0],
59 | [0, 0, 0, 0, 1, 1, 0, 0, 1, 0],
60 | [0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
61 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
62 | ]
63 |
64 |
65 | def main():
66 | print_map(MAP)
67 | begin = (0, 0)
68 | end = (len(MAP)-1, len(MAP)-1)
69 | for (row, col) in find_path(MAP, begin, end):
70 | MAP[row][col] = 'X'
71 | print_map(MAP)
72 |
73 |
74 | if __name__ == '__main__':
75 | main()
76 |
--------------------------------------------------------------------------------
/paip/sentences.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """
4 | Random sentence generation using context-free generative grammars.
5 |
6 | Translated from chapter 2 of "Paradigms of Artificial Intelligence
7 | Programming" by Peter Norvig.
8 | """
9 |
10 | __author__ = 'Daniel Connelly'
11 | __email__ = 'dconnelly@gatech.edu'
12 |
13 |
14 | import random
15 |
16 |
17 | SIMPLE_ENGLISH = {
18 | 'verb': ['hit', 'took', 'saw', 'liked'],
19 | 'noun': ['man', 'ball', 'woman', 'table'],
20 | 'article': ['the', 'a'],
21 |
22 | 'sentence': [['noun phrase', 'verb phrase']],
23 | 'noun phrase': [['article', 'noun']],
24 | 'verb phrase': [['verb', 'noun phrase']],
25 | }
26 |
27 |
28 | BIGGER_ENGLISH = {
29 | 'prep': ['to', 'in', 'by', 'with', 'on'],
30 | 'adj': ['big', 'little', 'blue', 'green', 'adiabatic'],
31 | 'article': ['a', 'the'],
32 | 'name': ['Pat', 'Kim', 'Lee', 'Terry', 'Robin'],
33 | 'noun': ['man', 'ball', 'woman', 'table'],
34 | 'verb': ['hit', 'took', 'saw', 'liked'],
35 | 'pronoun': ['he', 'she', 'it', 'these', 'those', 'that'],
36 |
37 | 'sentence': [['noun phrase', 'verb phrase']],
38 | 'noun phrase': [['article', 'adj*', 'noun', 'pp*'],
39 | ['name'],
40 | ['pronoun']],
41 | 'verb phrase': [['verb', 'noun phrase', 'pp*']],
42 | 'pp*': [[], ['pp']],
43 | 'adj*': [[], ['adj']],
44 | 'pp': [['prep', 'noun phrase']],
45 | }
46 |
47 |
48 | def generate(grammar, phrase):
49 | """Recursively rewrites each subphrase until only terminals remain.
50 |
51 | grammar is a dictionary defining a context-free grammar, where each
52 | (key, value) item defines a rewriting rule.
53 | Each subphrase of phrase is recursively rewritten unless it does not
54 | appear as a key in the grammar.
55 | """
56 | if isinstance(phrase, list):
57 | phrases = (generate(grammar, p) for p in phrase)
58 | return " ".join(p for p in phrases if p)
59 | elif phrase in grammar:
60 | return generate(grammar, random.choice(grammar[phrase]))
61 | else:
62 | return phrase
63 |
64 |
65 | def generate_tree(grammar, phrase):
66 | """Generates a sentence from the grammar and returns its parse tree.
67 |
68 | The sentence is generated in the same manner as in generate, but the
69 | returned value is a nested list where the first element of each sublist
70 | is the name of the rule generating the phrase.
71 | """
72 | if isinstance(phrase, list):
73 | return [generate_tree(grammar, p) for p in phrase]
74 | elif phrase in grammar:
75 | return [phrase] + generate_tree(grammar, random.choice(grammar[phrase]))
76 | else:
77 | return [phrase]
78 |
79 |
80 | if __name__ == '__main__':
81 | print generate(BIGGER_ENGLISH, 'sentence')
82 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | paip-python
2 | ===========
3 |
4 | Python implementations of some of the classic AI programs from Peter Norvig's
5 | fantastic textbook "Paradigms of Artificial Intelligence Programming."
6 |
7 | About
8 | -----
9 |
10 | This is meant to be a learning resource for beginning AI programmers. Although
11 | PAIP is a fantastic book, it is no longer common for students to have a
12 | background in Lisp programming, as many universities have replaced Lisp with
13 | other languages in introductory programming and introductory artificial
14 | intelligence courses. It is my hope that making the programs from PAIP
15 | available in a commonly-taught language will provide a useful hands-on resource
16 | for beginning AI students.
17 |
18 | The following programs are available, each with annotated source:
19 |
20 | - [General Problem Solver][GPS], means-ends analysis problem solving
21 | - [Eliza][], a pattern-matching psychiatrist
22 | - [Search][], a collection of search algorithms
23 | - [Logic][], a library for logic programming
24 | - [Prolog][], a basic Prolog interpreter
25 | - [Emycin][], an expert system shell
26 | - [Othello][], some game-playing strategies for the Othello board game
27 |
28 | Unit tests and some example applications are provided for each of these; see the
29 | `paip/tests` and `paip/examples` directories or the links from the annotated
30 | sources.
31 |
32 | [GPS]: http://dhconnelly.github.io/paip-python/docs/paip/gps.html
33 | [Eliza]: http://dhconnelly.github.io/paip-python/docs/paip/eliza.html
34 | [Search]: http://dhconnelly.github.io/paip-python/docs/paip/search.html
35 | [Logic]: http://dhconnelly.github.io/paip-python/docs/paip/logic.html
36 | [Prolog]: http://dhconnelly.github.io/paip-python/docs/prolog.html
37 | [Emycin]: http://dhconnelly.github.io/paip-python/docs/paip/emycin.html
38 | [Othello]: http://dhconnelly.github.io/paip-python/docs/paip/othello.html
39 |
40 | Getting Started
41 | ---------------
42 |
43 | Get the source code from [GitHub](https://github.com/dhconnelly/paip-python) or
44 | just [download](https://github.com/dhconnelly/paip-python/zipball/master) the
45 | latest version.
46 |
47 | Also make sure you have [Python 2.7](http://python.org/download/releases).
48 |
49 | - To run the examples: `python run_examples.py` and follow the prompts.
50 | - To run the Prolog interpreter: `./prolog.py`. Pass the `-h` flag for more
51 | details on its use and capabilities.
52 | - To run the unit tests: `python run_tests.py`.
53 | - To build the documentation: `python build_docs.py`.
54 |
55 | Contributing
56 | ------------
57 |
58 | - fork on [GitHub](https://github.com/dhconnelly/paip-python)
59 | - write code in `paip/`
60 | - add unit tests in `paip/tests`
61 | - make sure all tests pass: `python run_tests.py`
62 | - send me a pull request
63 |
64 | Author
65 | ------
66 |
67 | These programs were written by [Daniel Connelly](http://dhconnelly.com) at
68 | Georgia Tech as an independent project supervised by [Professor Ashok
69 | Goel](http://home.cc.gatech.edu/dil/3).
70 |
--------------------------------------------------------------------------------
/paip/tests/test_eliza.py:
--------------------------------------------------------------------------------
1 | from paip import eliza
2 | import unittest
3 |
4 |
5 | class TestIsVariable(unittest.TestCase):
6 | def test_is_variable(self):
7 | self.assertTrue(eliza.is_variable('?x'))
8 | self.assertTrue(eliza.is_variable('?subj'))
9 |
10 | def test_is_not_variable(self):
11 | self.assertFalse(eliza.is_variable('is it?'))
12 | self.assertFalse(eliza.is_variable('? why?'))
13 | self.assertFalse(eliza.is_variable('?'))
14 | self.assertFalse(eliza.is_variable(['?x']))
15 | self.assertFalse(eliza.is_variable('?foo bar'))
16 |
17 |
18 | class TestIsSegment(unittest.TestCase):
19 | def test_is_segment(self):
20 | self.assertTrue(eliza.is_segment(['?*foo', 'bar']))
21 | self.assertTrue(eliza.is_segment(['?*x']))
22 |
23 | def test_is_not_segment(self):
24 | self.assertFalse(eliza.is_segment('?*foo bar'))
25 | self.assertFalse(eliza.is_segment(['?*']))
26 | self.assertFalse(eliza.is_segment(['?*foo bar']))
27 |
28 |
29 | class TestContainsTokens(unittest.TestCase):
30 | def test_contains_tokens(self):
31 | self.assertTrue(eliza.contains_tokens(['foo', 'bar']))
32 |
33 | def test_does_not_contain_tokens(self):
34 | self.assertFalse(eliza.contains_tokens('foo bar'))
35 | self.assertFalse(eliza.contains_tokens([]))
36 |
37 |
38 | class TestMatchVariable(unittest.TestCase):
39 | def test_bind_unbound_variable(self):
40 | self.assertEqual(eliza.match_variable('foo', 'bar', {'baz': 'quux'}),
41 | {'baz': 'quux', 'foo': 'bar'})
42 |
43 | def test_bind_bound_variable_success(self):
44 | self.assertEqual(eliza.match_variable('foo', 'bar', {'foo': 'bar'}),
45 | {'foo': 'bar'})
46 |
47 | def test_bind_bound_variable_fail(self):
48 | self.assertFalse(eliza.match_variable('foo', 'bar', {'foo': 'baz'}))
49 |
50 |
51 | class TestMatchSegment(unittest.TestCase):
52 | def test_segment_match_rest(self):
53 | self.assertEqual(
54 | {'bar': 'baz', 'foo': ['bah', 'bla']},
55 | eliza.match_segment('foo', [], ['bah', 'bla'], {'bar': 'baz'}))
56 |
57 | def test_segment_first_match(self):
58 | self.assertEqual(
59 | {'foo': ['blue'], 'x': ['red']},
60 | eliza.match_segment('foo', ['is', '?x', 'today'],
61 | ['blue', 'is', 'red', 'today'], {}))
62 |
63 | def test_segment_second_match(self):
64 | phrase = 'blue is red today and today is tomorrow'
65 | self.assertEqual(
66 | {'foo': ['blue'], 'x': ['red', 'today', 'and'], 'y': ['tomorrow']},
67 | eliza.match_segment('foo', ['is', '?*x', 'today', 'is', '?y'],
68 | phrase.split(), {}))
69 |
70 | def test_segment_no_match(self):
71 | phrase = 'red is blue is not now'
72 | self.assertFalse(eliza.match_segment('foo', ['is', '?y', 'now', '?z'],
73 | phrase.split(), {}))
74 |
75 |
76 | class TestMatchPattern(unittest.TestCase):
77 | def test_no_more_vars(self):
78 | self.assertEqual({}, eliza.match_pattern(['hello', 'world'],
79 | ['hello', 'world'], {}))
80 |
81 | def test_match_no_more_vars_fail(self):
82 | self.assertFalse(eliza.match_pattern(['hello', 'world'],
83 | ['hello', 'bob'], {}))
84 |
85 | def test_match_segment(self):
86 | self.assertEqual({'x': ['hello', 'bob']},
87 | eliza.match_pattern(['?*x', 'world'],
88 | ['hello', 'bob', 'world'], {}))
89 |
90 | def test_match_var(self):
91 | self.assertEqual({'x': ['bob']}, eliza.match_pattern('?x', 'bob', {}))
92 |
93 | def test_match_pattern(self):
94 | self.assertEqual(
95 | {'y': ['bob'], 'x': ['john', 'jay']},
96 | eliza.match_pattern(
97 | 'hello ?y my name is ?*x pleased to meet you'.split(),
98 | 'hello bob my name is john jay pleased to meet you'.split(),
99 | {}))
100 |
101 | def test_empty_input(self):
102 | self.assertFalse(eliza.match_pattern(['foo', '?x'], [], {}))
103 |
104 | def test_empty_pattern(self):
105 | self.assertFalse(eliza.match_pattern([], ['foo', 'bar'], {}))
106 |
--------------------------------------------------------------------------------
/paip/abandoned/pattern.py:
--------------------------------------------------------------------------------
1 | class SyntaxError(Exception):
2 | def __init__(msg):
3 | self.msg = msg
4 |
5 | def __str__(msg):
6 | print 'Syntax error: %s' % self.msg
7 |
8 | class Binding(object):
9 | def __init__(self, var, type=None):
10 | self.var = var
11 | self.type = type
12 |
13 | def __repr__(self):
14 | return '%sBinding("%s")' % (self.type if self.type else '', self.var)
15 |
16 | class Constant(object):
17 | def __init__(self, val):
18 | self.val = val
19 |
20 | def __repr__(self):
21 | return '"%s"' % self.val
22 |
23 | class Pattern(object):
24 | def __init__(self, exprs):
25 | self.exprs = exprs
26 |
27 | def __repr__(self):
28 | return 'Pattern(%s)' % self.exprs
29 |
30 | class Block(object):
31 | AND, OR, NOT = 'and', 'or', 'not'
32 |
33 | def __init__(self, patterns, type):
34 | self.patterns = patterns
35 | self.type = type
36 |
37 | def __repr__(self):
38 | return '%s(%s)' % (self.type, self.patterns)
39 |
40 | class Scanner(object):
41 | def __init__(self, text):
42 | self.text = text
43 | self.index = 0
44 |
45 | def peek(self):
46 | if self.index == len(self.text):
47 | return None
48 | ch = self.text[self.index]
49 | return ch
50 |
51 | def get(self):
52 | ch = self.peek()
53 | self.index += 1
54 | return ch
55 |
56 | # Token types and recognizers
57 |
58 | SPACE = ' '
59 | NEWLINE = '\n'
60 | TAB = '\t'
61 | LPAREN = '('
62 | RPAREN = ')'
63 | COMMA = ','
64 | STAR = '*'
65 | PLUS = '+'
66 | QUESTION = '?'
67 |
68 | WHITESPACE = (SPACE, NEWLINE, TAB)
69 | def is_whitespace(token):
70 | return token in WHITESPACE
71 |
72 | SYMBOL = 'symbol'
73 | SYMBOLS = (LPAREN, RPAREN, COMMA, STAR, PLUS, QUESTION)
74 | def is_symbol(token):
75 | return token in SYMBOLS
76 |
77 | KEYWORD = 'keyword'
78 | KEYWORDS = (Block.AND, Block.OR, Block.NOT)
79 | def is_keyword(token):
80 | return token in KEYWORDS
81 |
82 | CONSTANT = 'constant'
83 | def is_constant(token):
84 | return token and token[0] != '?'
85 |
86 | class Lexer(object):
87 | def __init__(self, scanner):
88 | self.scanner = scanner
89 |
90 | def gettok(self):
91 | # skip over whitespace and make sure there's still input
92 | while is_whitespace(self.scanner.peek()):
93 | self.scanner.get()
94 | if not self.scanner.peek():
95 | return None
96 |
97 | # check if the next character is a delimiting symbol
98 | if self.scanner.peek() in SYMBOLS:
99 | symbol = self.scanner.get()
100 | return symbol, symbol
101 |
102 | # read the token up to the next whitespace
103 | token = ""
104 | while self.scanner.peek() not in (None,) + WHITESPACE + SYMBOLS:
105 | token += self.scanner.get()
106 |
107 | # determine token type and return
108 | if is_keyword(token):
109 | return KEYWORD, token
110 | elif is_constant(token):
111 | return CONSTANT, token
112 | else:
113 | raise Exception('Unrecognized token: %s' % token)
114 |
115 |
116 | class Parser(object):
117 | def __init__(self, lexer):
118 | self.lexer = lexer
119 | self.gettok()
120 |
121 | def gettok(self):
122 | got = self.lexer.gettok()
123 | if got:
124 | self.type, self.token = got
125 | else:
126 | self.type, self.token = None, None
127 |
128 | def found(self, type):
129 | if self.type == type:
130 | return True
131 | return False
132 |
133 | def consume(self, type=None):
134 | if type and self.type != type:
135 | raise SyntaxError('expected %s, got %s' % (type, self.type))
136 | token = self.token
137 | self.gettok()
138 | return token
139 |
140 | # pattern = expr*
141 | def pattern(self):
142 | exprs = []
143 | while self.token:
144 | try:
145 | exprs.append(self.expr())
146 | except:
147 | break
148 | return Pattern(exprs)
149 |
150 | # expr = CONSTANT | "?" binding | ("and" | "or" | "not") "(" block ")"
151 | def expr(self):
152 | if self.found(CONSTANT):
153 | return Constant(self.consume())
154 | elif self.found(QUESTION):
155 | self.gettok()
156 | return self.binding()
157 | elif self.found(KEYWORD):
158 | which = self.consume()
159 | self.consume(LPAREN)
160 | block = self.block()
161 | self.consume(RPAREN)
162 | return Block(block, which)
163 | else:
164 | raise SyntaxError('expr cannot start with %s' % self.token)
165 |
166 | # binding = ("*" | "+" | "?")? CONST
167 | def binding(self):
168 | type = None
169 | if self.found(STAR) or self.found(PLUS) or self.found(QUESTION):
170 | type = self.consume()
171 | const = self.consume(CONSTANT)
172 | return Binding(const, type)
173 |
174 | # block = pattern [, pattern]*
175 | def block(self):
176 | patterns = [self.pattern()]
177 | while self.found(COMMA):
178 | self.gettok()
179 | patterns.append(self.pattern())
180 | return patterns
181 |
182 | def parse(pattern):
183 | p = Parser(Lexer(Scanner(pattern)))
184 | return p.pattern()
185 |
186 |
--------------------------------------------------------------------------------
/paip/tests/test_gps.py:
--------------------------------------------------------------------------------
1 | from paip import gps
2 | import unittest
3 |
4 |
5 | throw = {'action': 'throw baseball',
6 | 'preconds': ['have baseball', 'arm up'],
7 | 'add': ['arm down', 'baseball in air', 'throwing baseball'],
8 | 'delete': ['have baseball', 'arm up']}
9 |
10 | raise_arm = {'action': 'raise arm',
11 | 'preconds': ['arm down'],
12 | 'add': ['arm up', 'raising arm'],
13 | 'delete': ['arm down']}
14 |
15 | grab_baseball = {'action': 'grab baseball',
16 | 'preconds': ['hand empty', 'arm down'],
17 | 'add': ['have baseball', 'grabbing baseball'],
18 | 'delete': ['hand empty']}
19 |
20 | drink_beer = {'action': 'drink beer',
21 | 'preconds': ['arm down', 'hand empty'],
22 | 'add': ['satisfied', 'drinking beer'],
23 | 'delete': []}
24 |
25 | ops = [throw, raise_arm, grab_baseball, drink_beer]
26 |
27 |
28 | class TestApplyOperator(unittest.TestCase):
29 | def test_apply_operator_preconds_satisfied(self):
30 | current = ['have baseball', 'arm up', 'have food']
31 | expected = ['arm down',
32 | 'baseball in air',
33 | 'have food',
34 | 'throwing baseball']
35 | goal = 'baseball in air'
36 | final = gps.apply_operator(throw, current, ops, goal, [])
37 | self.assertEqual(set(final), set(expected))
38 |
39 | def test_apply_operator_recurse(self):
40 | current = ['hand empty', 'arm down', 'have food']
41 | expected = ['arm down',
42 | 'baseball in air',
43 | 'have food',
44 | 'grabbing baseball',
45 | 'raising arm',
46 | 'throwing baseball']
47 | goal = 'baseball in air'
48 | final = gps.apply_operator(throw, current, ops, goal, [])
49 | self.assertEqual(set(final), set(expected))
50 |
51 | def test_apply_operator_recurse_fail(self):
52 | current = ['hand empty', 'have food', 'arm up']
53 | goal = 'baseball in air'
54 | self.assertFalse(gps.apply_operator(throw, current, ops, goal, []))
55 |
56 |
57 | class TestAchieve(unittest.TestCase):
58 | def test_achieve_already_done(self):
59 | expected = ['hand empty', 'arm down']
60 | final = gps.achieve(expected, ops, 'arm down', [])
61 | self.assertEqual(set(expected), set(final))
62 |
63 | def test_achieve_prevent_loop(self):
64 | current = ['hand empty', 'arm down']
65 | goal = 'baseball in air'
66 | stack = ['baseball in air', 'levitate']
67 | self.assertFalse(gps.achieve(current, ops, goal, stack))
68 |
69 | def test_achieve(self):
70 | current = ['hand empty', 'arm down']
71 | goal = 'baseball in air'
72 | expected = ['arm down',
73 | 'baseball in air',
74 | 'grabbing baseball',
75 | 'raising arm',
76 | 'throwing baseball']
77 | final = gps.achieve(current, ops, goal, [])
78 | self.assertEqual(set(expected), set(final))
79 |
80 | def test_achieve_try_another_op(self):
81 | current = ['hand empty', 'arm down']
82 | levitate_baseball = {'action': 'levitate baseball',
83 | 'preconds': ['magic'],
84 | 'add': ['baseball in air'],
85 | 'delete': []}
86 | goal = 'baseball in air'
87 | new_ops = [levitate_baseball] + ops
88 | final = gps.achieve(current, new_ops, goal, [])
89 | expected = ['arm down',
90 | 'baseball in air',
91 | 'grabbing baseball',
92 | 'raising arm',
93 | 'throwing baseball']
94 | self.assertEqual(set(expected), set(final))
95 |
96 |
97 | class TestAchieveAll(unittest.TestCase):
98 | def test_achieve_all(self):
99 | current = ['hand empty', 'arm down']
100 | goals = ['satisfied', 'baseball in air']
101 | expected = ['satisfied',
102 | 'arm down',
103 | 'baseball in air',
104 | 'grabbing baseball',
105 | 'raising arm',
106 | 'throwing baseball',
107 | 'drinking beer']
108 | final = gps.achieve_all(current, ops, goals, [])
109 | self.assertEqual(set(expected), set(final))
110 |
111 | def test_achieve_all_one_impossible(self):
112 | current = ['hand empty', 'arm down']
113 | goals = ['satisfied', 'baseball in air', 'awesome']
114 | self.assertFalse(gps.achieve_all(current, ops, goals, []))
115 |
116 | def test_achieve_all_clobbers_sibling(self):
117 | current = ['hand empty', 'arm down']
118 | goals = ['baseball in air', 'satisfied']
119 | self.assertFalse(gps.achieve_all(current, ops, goals, []))
120 |
121 |
122 | class TestGps(unittest.TestCase):
123 | def test_gps(self):
124 | current = ['hand empty', 'arm down']
125 | goals = ['satisfied', 'baseball in air']
126 | expected = ['Executing drink beer',
127 | 'Executing grab baseball',
128 | 'Executing raise arm',
129 | 'Executing throw baseball']
130 | final = gps.gps(current, goals, ops)
131 | self.assertEqual(final, expected) # order matters this time
132 |
133 | def test_gps_fail(self):
134 | current = ['hand empty', 'arm down']
135 | goals = ['satisfied', 'baseball in air', 'awesome']
136 | self.assertFalse(gps.gps(current, goals, ops))
137 |
--------------------------------------------------------------------------------
/paip/gps.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """
4 | The **General Problem Solver** is a framework for applying *means-ends analysis*
5 | to solve problems that can be specified by a list of initial states, a list of
6 | goal states, and a list of operators that induce state transitions.
7 |
8 | Each operator is specified by an action name, a list of precondition states that
9 | must hold before the operator is applied, a list of states that will hold after
10 | the operator is applied (the *add-list*), and a list of states that will no
11 | longer hold after the operator is applied (the *delete-list*). To achieve a
12 | goal state, GPS uses means-ends analysis: each operator is examined to find one
13 | that contains the goal state in its add-list (it looks for an *appropriate*
14 | operator). It then tries to achieve all of that operator's precondition states.
15 | If not all of the preconditions can be achieved (the operator is not
16 | *applicable*), then GPS looks for another appropriate operator. If none exists,
17 | then the goal can't be achieved. When all of the goal states have been
18 | achieved, the problem is solved.
19 |
20 | The following programs demonstrate using GPS to solve some famous AI problems:
21 |
22 | - [Monkey and Bananas](examples/gps/monkeys.html)
23 | - [Blocks World](examples/gps/blocks.html)
24 | - [Driving to school](examples/gps/school.html)
25 |
26 | This implementation is inspired by chapter 4 of "Paradigms of Artificial
27 | Intelligence Programming" by Peter Norvig.
28 | """
29 |
30 | ## General Problem Solver
31 |
32 | def gps(initial_states, goal_states, operators):
33 | """
34 | Find a sequence of operators that will achieve all of the goal states.
35 |
36 | Returns a list of actions that will achieve all of the goal states, or
37 | None if no such sequence exists. Each operator is specified by an
38 | action name, a list of preconditions, an add-list, and a delete-list.
39 | """
40 |
41 | # To keep track of which operators have been applied, we add additional
42 | # 'executing ...' states to each operator's add-list. These will never be
43 | # deleted by another operator, so when the problem is solved we can find
44 | # them in the list of current states.
45 | prefix = 'Executing '
46 | for operator in operators:
47 | operator['add'].append(prefix + operator['action'])
48 |
49 | final_states = achieve_all(initial_states, operators, goal_states, [])
50 | if not final_states:
51 | return None
52 | return [state for state in final_states if state.startswith(prefix)]
53 |
54 |
55 | ## Achieving subgoals
56 |
57 | def achieve_all(states, ops, goals, goal_stack):
58 | """
59 | Achieve each state in goals and make sure they still hold at the end.
60 |
61 | The goal stack keeps track of our recursion: which preconditions are we
62 | trying to satisfy by achieving the specified goals?
63 | """
64 |
65 | # We try to achieve each goal in the order they are given. If any one
66 | # goal state cannot be achieved, then the problem cannot be solved.
67 | for goal in goals:
68 | states = achieve(states, ops, goal, goal_stack)
69 | if not states:
70 | return None
71 |
72 | # We must ensure that we haven't removed a goal state in the process of
73 | # solving other states--having done so is called the "prerequisite clobbers
74 | # sibling goal problem".
75 | for goal in goals:
76 | if goal not in states:
77 | return None
78 |
79 | return states
80 |
81 |
82 | def achieve(states, operators, goal, goal_stack):
83 | """
84 | Achieve the goal state using means-ends analysis.
85 |
86 | Identifies an appropriate and applicable operator--one that contains the
87 | goal state in its add-list and has all its preconditions satisified.
88 | Applies the operator and returns the result. Returns None if no such
89 | operator is found or infinite recursion is detected in the goal stack.
90 | """
91 |
92 | debug(len(goal_stack), 'Achieving: %s' % goal)
93 |
94 | # Let's check to see if the state already holds before we do anything.
95 | if goal in states:
96 | return states
97 |
98 | # Prevent going in circles: look through the goal stack to see if the
99 | # specified goal appears there. If so, then we are indirectly trying to
100 | # achieve goal while already in the process of achieving it. For example,
101 | # while trying to achieve state A, we try to achieve state B--a precondition
102 | # for applying an appropriate operator. However, to achieve B, we try to
103 | # satisfy the preconditions for another operator that contains A in its
104 | # preconditions.
105 | if goal in goal_stack:
106 | return None
107 |
108 | for op in operators:
109 | # Is op appropriate? Look through its add-list to see if goal is there.
110 | if goal not in op['add']:
111 | continue
112 | # Is op applicable? Try to apply it--if one of its preconditions cannot
113 | # be satisifed, then it will return None.
114 | result = apply_operator(op, states, operators, goal, goal_stack)
115 | if result:
116 | return result
117 |
118 |
119 | ## Using operators
120 |
121 | def apply_operator(operator, states, ops, goal, goal_stack):
122 | """
123 | Applies operator and returns the resulting states.
124 |
125 | Achieves all of operator's preconditions and returns the states that hold
126 | after processing its add-list and delete-list. If any of its preconditions
127 | cannot be satisfied, returns None.
128 | """
129 |
130 | debug(len(goal_stack), 'Consider: %s' % operator['action'])
131 |
132 | # Satisfy all of operator's preconditions.
133 | result = achieve_all(states, ops, operator['preconds'], [goal] + goal_stack)
134 | if not result:
135 | return None
136 |
137 | debug(len(goal_stack), 'Action: %s' % operator['action'])
138 |
139 | # Merge the old states with operator's add-list, filtering out delete-list.
140 | add_list, delete_list = operator['add'], operator['delete']
141 | return [state for state in result if state not in delete_list] + add_list
142 |
143 |
144 | ## Helper functions
145 |
146 | import logging
147 |
148 | def debug(level, msg):
149 | logging.debug(' %s %s' % (level * ' ', msg))
150 |
--------------------------------------------------------------------------------
/paip/examples/emycin/mycin.py:
--------------------------------------------------------------------------------
1 | """
2 | Mycin: a medical expert system.
3 |
4 | This is a small example of an expert system that uses the
5 | [Emycin](../../emycin.html) shell. It defines a few contexts, parameters, and
6 | rules, and presents a rudimentary user interface to collect data about an
7 | infection in order to determine the identity of the infecting organism.
8 |
9 | In a more polished system, we could:
10 |
11 | - define and use a domain-specific language for the expert;
12 | - present a more polished interface, perhaps a GUI, for user interaction;
13 | - offer a data serialization mechanism to save state between sessions.
14 |
15 | This implementation comes from chapter 16 of Peter Norvig's "Paradigms of
16 | Artificial Intelligence Programming.
17 |
18 | """
19 |
20 | ### Utility functions
21 |
22 | def eq(x, y):
23 | """Function for testing value equality."""
24 | return x == y
25 |
26 | def boolean(string):
27 | """
28 | Function for reading True or False from a string. Raises an error if the
29 | string is not True or False.
30 | """
31 | if string == 'True':
32 | return True
33 | if string == 'False':
34 | return False
35 | raise ValueError('bool must be True or False')
36 |
37 |
38 | ### Setting up initial data
39 |
40 | # Here we define the contexts, parameters, and rules for our system. This is
41 | # the job of the expert, and in a more polished system, we would define and use
42 | # a domain-specific language to make this easier.
43 |
44 | def define_contexts(sh):
45 | # Patient and Culture have some initial goals--parameters that should be
46 | # collected before reasoning begins. This might be useful in some domains;
47 | # for example, this might be legally required in a medical system.
48 | sh.define_context(Context('patient', ['name', 'sex', 'age']))
49 | sh.define_context(Context('culture', ['site', 'days-old']))
50 |
51 | # Finding the identity of the organism is our goal.
52 | sh.define_context(Context('organism', goals=['identity']))
53 |
54 | def define_params(sh):
55 | # Patient params
56 | sh.define_param(Parameter('name', 'patient', cls=str, ask_first=True))
57 | sh.define_param(Parameter('sex', 'patient', enum=['M', 'F'], ask_first=True))
58 | sh.define_param(Parameter('age', 'patient', cls=int, ask_first=True))
59 | sh.define_param(Parameter('burn', 'patient',
60 | enum=['no', 'mild', 'serious'], ask_first=True))
61 | sh.define_param(Parameter('compromised-host', 'patient', cls=boolean))
62 |
63 | # Culture params
64 | sh.define_param(Parameter('site', 'culture', enum=['blood'], ask_first=True))
65 | sh.define_param(Parameter('days-old', 'culture', cls=int, ask_first=True))
66 |
67 | # Organism params
68 | organisms = ['pseudomonas', 'klebsiella', 'enterobacteriaceae',
69 | 'staphylococcus', 'bacteroides', 'streptococcus']
70 | sh.define_param(Parameter('identity', 'organism', enum=organisms, ask_first=True))
71 | sh.define_param(Parameter('gram', 'organism',
72 | enum=['acid-fast', 'pos', 'neg'], ask_first=True))
73 | sh.define_param(Parameter('morphology', 'organism', enum=['rod', 'coccus']))
74 | sh.define_param(Parameter('aerobicity', 'organism', enum=['aerobic', 'anaerobic']))
75 | sh.define_param(Parameter('growth-conformation', 'organism',
76 | enum=['chains', 'pairs', 'clumps']))
77 |
78 | def define_rules(sh):
79 | sh.define_rule(Rule(52,
80 | [('site', 'culture', eq, 'blood'),
81 | ('gram', 'organism', eq, 'neg'),
82 | ('morphology', 'organism', eq, 'rod'),
83 | ('burn', 'patient', eq, 'serious')],
84 | [('identity', 'organism', eq, 'pseudomonas')],
85 | 0.4))
86 | sh.define_rule(Rule(71,
87 | [('gram', 'organism', eq, 'pos'),
88 | ('morphology', 'organism', eq, 'coccus'),
89 | ('growth-conformation', 'organism', eq, 'clumps')],
90 | [('identity', 'organism', eq, 'staphylococcus')],
91 | 0.7))
92 | sh.define_rule(Rule(73,
93 | [('site', 'culture', eq, 'blood'),
94 | ('gram', 'organism', eq, 'neg'),
95 | ('morphology', 'organism', eq, 'rod'),
96 | ('aerobicity', 'organism', eq, 'anaerobic')],
97 | [('identity', 'organism', eq, 'bacteroides')],
98 | 0.9))
99 | sh.define_rule(Rule(75,
100 | [('gram', 'organism', eq, 'neg'),
101 | ('morphology', 'organism', eq, 'rod'),
102 | ('compromised-host', 'patient', eq, True)],
103 | [('identity', 'organism', eq, 'pseudomonas')],
104 | 0.6))
105 | sh.define_rule(Rule(107,
106 | [('gram', 'organism', eq, 'neg'),
107 | ('morphology', 'organism', eq, 'rod'),
108 | ('aerobicity', 'organism', eq, 'aerobic')],
109 | [('identity', 'organism', eq, 'enterobacteriaceae')],
110 | 0.8))
111 | sh.define_rule(Rule(165,
112 | [('gram', 'organism', eq, 'pos'),
113 | ('morphology', 'organism', eq, 'coccus'),
114 | ('growth-conformation', 'organism', eq, 'chains')],
115 | [('identity', 'organism', eq, 'streptococcus')],
116 | 0.7))
117 |
118 |
119 | ### Running the system
120 |
121 | import logging
122 | from paip.emycin import Parameter, Context, Rule, Shell
123 |
124 | def report_findings(findings):
125 | for inst, result in findings.items():
126 | print 'Findings for %s-%d:' % (inst[0], inst[1])
127 | for param, vals in result.items():
128 | possibilities = ['%s: %f' % (val[0], val[1]) for val in vals.items()]
129 | print '%s: %s' % (param, ', '.join(possibilities))
130 |
131 | def main():
132 | sh = Shell()
133 | define_contexts(sh)
134 | define_params(sh)
135 | define_rules(sh)
136 | report_findings(sh.execute(['patient', 'culture', 'organism']))
137 |
--------------------------------------------------------------------------------
/paip/examples/eliza/eliza.py:
--------------------------------------------------------------------------------
1 | import json
2 | import sys
3 | from paip import eliza
4 |
5 | rules = {
6 | "?*x hello ?*y": [
7 | "How do you do. Please state your problem."
8 | ],
9 | "?*x computer ?*y": [
10 | "Do computers worry you?",
11 | "What do you think about machines?",
12 | "Why do you mention computers?",
13 | "What do you think machines have to do with your problem?",
14 | ],
15 | "?*x name ?*y": [
16 | "I am not interested in names",
17 | ],
18 | "?*x sorry ?*y": [
19 | "Please don't apologize",
20 | "Apologies are not necessary",
21 | "What feelings do you have when you apologize",
22 | ],
23 | "?*x I remember ?*y": [
24 | "Do you often think of ?y?",
25 | "Does thinking of ?y bring anything else to mind?",
26 | "What else do you remember?",
27 | "Why do you recall ?y right now?",
28 | "What in the present situation reminds you of ?y?",
29 | "What is the connection between me and ?y?",
30 | ],
31 | "?*x do you remember ?*y": [
32 | "Did you think I would forget ?y?",
33 | "Why do you think I should recall ?y now?",
34 | "What about ?y?",
35 | "You mentioned ?y",
36 | ],
37 | "?*x I want ?*y": [
38 | "What would it mean if you got ?y?",
39 | "Why do you want ?y?",
40 | "Suppose you got ?y soon."
41 | ],
42 | "?*x if ?*y": [
43 | "Do you really think it's likely that ?y?",
44 | "Do you wish that ?y?",
45 | "What do you think about ?y?",
46 | "Really--if ?y?"
47 | ],
48 | "?*x I dreamt ?*y": [
49 | "How do you feel about ?y in reality?",
50 | ],
51 | "?*x dream ?*y": [
52 | "What does this dream suggest to you?",
53 | "Do you dream often?",
54 | "What persons appear in your dreams?",
55 | "Don't you believe that dream has to do with your problem?",
56 | ],
57 | "?*x my mother ?*y": [
58 | "Who else in your family ?y?",
59 | "Tell me more about your family",
60 | ],
61 | "?*x my father ?*y": [
62 | "Your father?",
63 | "Does he influence you strongly?",
64 | "What else comes to mind when you think of your father?",
65 | ],
66 | "?*x I am glad ?*y": [
67 | "How have I helped you to be ?y?",
68 | "What makes you happy just now?",
69 | "Can you explain why you are suddenly ?y?",
70 | ],
71 | "?*x I am sad ?*y": [
72 | "I am sorry to hear you are depressed",
73 | "I'm sure it's not pleasant to be sad",
74 | ],
75 | "?*x are like ?*y": [
76 | "What resemblence do you see between ?x and ?y?",
77 | ],
78 | "?*x is like ?*y": [
79 | "In what way is it that ?x is like ?y?",
80 | "What resemblence do you see?",
81 | "Could there really be some connection?",
82 | "How?",
83 | ],
84 | "?*x alike ?*y": [
85 | "In what way?",
86 | "What similarities are there?",
87 | ],
88 | "?* same ?*y": [
89 | "What other connections do you see?",
90 | ],
91 | "?*x no ?*y": [
92 | "Why not?",
93 | "You are being a bit negative.",
94 | "Are you saying 'No' just to be negative?"
95 | ],
96 | "?*x I was ?*y": [
97 | "Were you really?",
98 | "Perhaps I already knew you were ?y.",
99 | "Why do you tell me you were ?y now?"
100 | ],
101 | "?*x was I ?*y": [
102 | "What if you were ?y?",
103 | "Do you think you were ?y?",
104 | "What would it mean if you were ?y?",
105 | ],
106 | "?*x I am ?*y": [
107 | "In what way are you ?y?",
108 | "Do you want to be ?y?",
109 | ],
110 | "?*x am I ?*y": [
111 | "Do you believe you are ?y?",
112 | "Would you want to be ?y?",
113 | "You wish I would tell you you are ?y?",
114 | "What would it mean if you were ?y?",
115 | ],
116 | "?*x am ?*y": [
117 | "Why do you say 'AM?'",
118 | "I don't understand that"
119 | ],
120 | "?*x are you ?*y": [
121 | "Why are you interested in whether I am ?y or not?",
122 | "Would you prefer if I weren't ?y?",
123 | "Perhaps I am ?y in your fantasies",
124 | ],
125 | "?*x you are ?*y": [
126 | "What makes you think I am ?y?",
127 | ],
128 | "?*x because ?*y": [
129 | "Is that the real reason?",
130 | "What other reasons might there be?",
131 | "Does that reason seem to explain anything else?",
132 | ],
133 | "?*x were you ?*y": [
134 | "Perhaps I was ?y?",
135 | "What do you think?",
136 | "What if I had been ?y?",
137 | ],
138 | "?*x I can't ?*y": [
139 | "Maybe you could ?y now",
140 | "What if you could ?y?",
141 | ],
142 | "?*x I feel ?*y": [
143 | "Do you often feel ?y?"
144 | ],
145 | "?*x I felt ?*y": [
146 | "What other feelings do you have?"
147 | ],
148 | "?*x I ?*y you ?*z": [
149 | "Perhaps in your fantasy we ?y each other",
150 | ],
151 | "?*x why don't you ?*y": [
152 | "Should you ?y yourself?",
153 | "Do you believe I don't ?y?",
154 | "Perhaps I will ?y in good time",
155 | ],
156 | "?*x yes ?*y": [
157 | "You seem quite positive",
158 | "You are sure?",
159 | "I understand",
160 | ],
161 | "?*x someone ?*y": [
162 | "Can you be more specific?",
163 | ],
164 | "?*x everyone ?*y": [
165 | "Surely not everyone",
166 | "Can you think of anyone in particular?",
167 | "Who, for example?",
168 | "You are thinking of a special person",
169 | ],
170 | "?*x always ?*y": [
171 | "Can you think of a specific example?",
172 | "When?",
173 | "What incident are you thinking of?",
174 | "Really--always?",
175 | ],
176 | "?*x what ?*y": [
177 | "Why do you ask?",
178 | "Does that question interest you?",
179 | "What is it you really want to know?",
180 | "What do you think?",
181 | "What comes to your mind when you ask that?",
182 | ],
183 | "?*x perhaps ?*y": [
184 | "You do not seem quite certain",
185 | ],
186 | "?*x are ?*y": [
187 | "Did you think they might not be ?y?",
188 | "Possibly they are ?y",
189 | ],
190 | }
191 |
192 | default_responses = [
193 | "Very interesting",
194 | "I am not sure I understand you fully",
195 | "What does that suggest to you?",
196 | "Please continue",
197 | "Go on",
198 | "Do you feel strongly about discussing such things?",
199 | ]
200 |
201 | def main():
202 | # We need the rules in a list containing elements of the following form:
203 | # `(input pattern, [output pattern 1, output pattern 2, ...]`
204 | rules_list = []
205 | for pattern, transforms in rules.items():
206 | # Remove the punctuation from the pattern to simplify matching.
207 | pattern = eliza.remove_punct(str(pattern.upper())) # kill unicode
208 | transforms = [str(t).upper() for t in transforms]
209 | rules_list.append((pattern, transforms))
210 | eliza.interact('ELIZA> ', rules_list, map(str.upper, default_responses))
211 |
212 | if __name__ == '__main__':
213 | main(sys.argv[1:])
214 |
--------------------------------------------------------------------------------
/paip/examples/gps/blocks.py:
--------------------------------------------------------------------------------
1 | from paip.gps import gps
2 |
3 | problem = {
4 | "start": ["space on a", "a on b", "b on c", "c on table", "space on table"],
5 | "finish": ["space on c", "c on b", "b on a", "a on table", "space on table"],
6 | "ops": [
7 | {
8 | "action": "move a from b to c",
9 | "preconds": [
10 | "space on a",
11 | "space on c",
12 | "a on b"
13 | ],
14 | "add": [
15 | "a on c",
16 | "space on b"
17 | ],
18 | "delete": [
19 | "a on b",
20 | "space on c"
21 | ]
22 | },
23 | {
24 | "action": "move a from table to b",
25 | "preconds": [
26 | "space on a",
27 | "space on b",
28 | "a on table"
29 | ],
30 | "add": [
31 | "a on b"
32 | ],
33 | "delete": [
34 | "a on table",
35 | "space on b"
36 | ]
37 | },
38 | {
39 | "action": "move a from b to table",
40 | "preconds": [
41 | "space on a",
42 | "space on table",
43 | "a on b"
44 | ],
45 | "add": [
46 | "a on table",
47 | "space on b"
48 | ],
49 | "delete": [
50 | "a on b"
51 | ]
52 | },
53 | {
54 | "action": "move a from c to b",
55 | "preconds": [
56 | "space on a",
57 | "space on b",
58 | "a on c"
59 | ],
60 | "add": [
61 | "a on b",
62 | "space on c"
63 | ],
64 | "delete": [
65 | "a on c",
66 | "space on b"
67 | ]
68 | },
69 | {
70 | "action": "move a from table to c",
71 | "preconds": [
72 | "space on a",
73 | "space on c",
74 | "a on table"
75 | ],
76 | "add": [
77 | "a on c"
78 | ],
79 | "delete": [
80 | "a on table",
81 | "space on c"
82 | ]
83 | },
84 | {
85 | "action": "move a from c to table",
86 | "preconds": [
87 | "space on a",
88 | "space on table",
89 | "a on c"
90 | ],
91 | "add": [
92 | "a on table",
93 | "space on c"
94 | ],
95 | "delete": [
96 | "a on c"
97 | ]
98 | },
99 | {
100 | "action": "move b from a to c",
101 | "preconds": [
102 | "space on b",
103 | "space on c",
104 | "b on a"
105 | ],
106 | "add": [
107 | "b on c",
108 | "space on a"
109 | ],
110 | "delete": [
111 | "b on a",
112 | "space on c"
113 | ]
114 | },
115 | {
116 | "action": "move b from table to a",
117 | "preconds": [
118 | "space on b",
119 | "space on a",
120 | "b on table"
121 | ],
122 | "add": [
123 | "b on a"
124 | ],
125 | "delete": [
126 | "b on table",
127 | "space on a"
128 | ]
129 | },
130 | {
131 | "action": "move b from a to table",
132 | "preconds": [
133 | "space on b",
134 | "space on table",
135 | "b on a"
136 | ],
137 | "add": [
138 | "b on table",
139 | "space on a"
140 | ],
141 | "delete": [
142 | "b on a"
143 | ]
144 | },
145 | {
146 | "action": "move b from c to a",
147 | "preconds": [
148 | "space on b",
149 | "space on a",
150 | "b on c"
151 | ],
152 | "add": [
153 | "b on a",
154 | "space on c"
155 | ],
156 | "delete": [
157 | "b on c",
158 | "space on a"
159 | ]
160 | },
161 | {
162 | "action": "move b from table to c",
163 | "preconds": [
164 | "space on b",
165 | "space on c",
166 | "b on table"
167 | ],
168 | "add": [
169 | "b on c"
170 | ],
171 | "delete": [
172 | "b on table",
173 | "space on c"
174 | ]
175 | },
176 | {
177 | "action": "move b from c to table",
178 | "preconds": [
179 | "space on b",
180 | "space on table",
181 | "b on c"
182 | ],
183 | "add": [
184 | "b on table",
185 | "space on c"
186 | ],
187 | "delete": [
188 | "b on c"
189 | ]
190 | },
191 | {
192 | "action": "move c from a to b",
193 | "preconds": [
194 | "space on c",
195 | "space on b",
196 | "c on a"
197 | ],
198 | "add": [
199 | "c on b",
200 | "space on a"
201 | ],
202 | "delete": [
203 | "c on a",
204 | "space on b"
205 | ]
206 | },
207 | {
208 | "action": "move c from table to a",
209 | "preconds": [
210 | "space on c",
211 | "space on a",
212 | "c on table"
213 | ],
214 | "add": [
215 | "c on a"
216 | ],
217 | "delete": [
218 | "c on table",
219 | "space on a"
220 | ]
221 | },
222 | {
223 | "action": "move c from a to table",
224 | "preconds": [
225 | "space on c",
226 | "space on table",
227 | "c on a"
228 | ],
229 | "add": [
230 | "c on table",
231 | "space on a"
232 | ],
233 | "delete": [
234 | "c on a"
235 | ]
236 | },
237 | {
238 | "action": "move c from b to a",
239 | "preconds": [
240 | "space on c",
241 | "space on a",
242 | "c on b"
243 | ],
244 | "add": [
245 | "c on a",
246 | "space on b"
247 | ],
248 | "delete": [
249 | "c on b",
250 | "space on a"
251 | ]
252 | },
253 | {
254 | "action": "move c from table to b",
255 | "preconds": [
256 | "space on c",
257 | "space on b",
258 | "c on table"
259 | ],
260 | "add": [
261 | "c on b"
262 | ],
263 | "delete": [
264 | "c on table",
265 | "space on b"
266 | ]
267 | },
268 | {
269 | "action": "move c from b to table",
270 | "preconds": [
271 | "space on c",
272 | "space on table",
273 | "c on b"
274 | ],
275 | "add": [
276 | "c on table",
277 | "space on b"
278 | ],
279 | "delete": [
280 | "c on b"
281 | ]
282 | }
283 | ]
284 | }
285 |
286 | def main():
287 | start = problem['start']
288 | finish = problem['finish']
289 | ops = problem['ops']
290 | for action in gps(start, finish, ops):
291 | print action
292 |
293 | if __name__ == '__main__':
294 | main()
295 |
--------------------------------------------------------------------------------
/paip/eliza.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """
4 | **Eliza** is a pattern-matching automated psychiatrist. Given a set of rules
5 | in the form of input/output patterns, Eliza will attempt to recognize user input
6 | phrases and generate relevant psychobabble responses.
7 |
8 | Each rule is specified by an input pattern and a list of output patterns. A
9 | pattern is a sentence consisting of space-separated words and variables. Input
10 | pattern variables come in two forms: single variables and segment variables;
11 | single variables (which take the form `?x`) match a single word, while segment
12 | variables (which take the form `?*x`) can match a phrase. Output pattern
13 | variables are only single variables. The variable names contained in an input
14 | pattern should be the same as those in the corresponding output pattern, and
15 | each segment variable `?*x` in an input pattern corresponds to the single
16 | variable `?x` in the output pattern.
17 |
18 | The conversation proceeds by reading a sentence from the user, searching through
19 | the rules to find an input pattern that matches, replacing variables in the
20 | output pattern, and printing the results to the user.
21 |
22 | For examples of using this scheme, see the following programs:
23 |
24 | - [Eliza](examples/eliza/eliza.html)
25 | - [Automated technical support system](examples/eliza/support.html)
26 |
27 | This implementation is inspired by Chapter 5 of "Paradigms of Artificial
28 | Intelligence Programming" by Peter Norvig.
29 | """
30 |
31 | import random
32 | import string
33 |
34 | ## Talking to the computer
35 |
36 | def interact(prompt, rules, default_responses):
37 | """Have a conversation with a user."""
38 | # Read a line, process it, and print the results until no input remains.
39 | while True:
40 | try:
41 | # Remove the punctuation from the input and convert to upper-case
42 | # to simplify matching.
43 | input = remove_punct(raw_input(prompt).upper())
44 | if not input:
45 | continue
46 | except:
47 | break
48 | print respond(rules, input, default_responses)
49 |
50 |
51 | def respond(rules, input, default_responses):
52 | """Respond to an input sentence according to the given rules."""
53 |
54 | input = input.split() # match_pattern expects a list of tokens
55 |
56 | # Look through rules and find input patterns that matches the input.
57 | matching_rules = []
58 | for pattern, transforms in rules:
59 | pattern = pattern.split()
60 | replacements = match_pattern(pattern, input)
61 | if replacements:
62 | matching_rules.append((transforms, replacements))
63 |
64 | # When rules are found, choose one and one of its responses at random.
65 | # If no rule applies, we use the default rule.
66 | if matching_rules:
67 | responses, replacements = random.choice(matching_rules)
68 | response = random.choice(responses)
69 | else:
70 | replacements = {}
71 | response = random.choice(default_responses)
72 |
73 | # Replace the variables in the output pattern with the values matched from
74 | # the input string.
75 | for variable, replacement in replacements.items():
76 | replacement = ' '.join(switch_viewpoint(replacement))
77 | if replacement:
78 | response = response.replace('?' + variable, replacement)
79 |
80 | return response
81 |
82 |
83 | ## Pattern matching
84 |
85 | def match_pattern(pattern, input, bindings=None):
86 | """
87 | Determine if the input string matches the given pattern.
88 |
89 | Expects pattern and input to be lists of tokens, where each token is a word
90 | or a variable.
91 |
92 | Returns a dictionary containing the bindings of variables in the input
93 | pattern to values in the input string, or False when the input doesn't match
94 | the pattern.
95 | """
96 |
97 | # Check to see if matching failed before we got here.
98 | if bindings is False:
99 | return False
100 |
101 | # When the pattern and the input are identical, we have a match, and
102 | # no more bindings need to be found.
103 | if pattern == input:
104 | return bindings
105 |
106 | bindings = bindings or {}
107 |
108 | # Match input and pattern according to their types.
109 | if is_segment(pattern):
110 | token = pattern[0] # segment variable is the first token
111 | var = token[2:] # segment variable is of the form ?*x
112 | return match_segment(var, pattern[1:], input, bindings)
113 | elif is_variable(pattern):
114 | var = pattern[1:] # single variables are of the form ?foo
115 | return match_variable(var, [input], bindings)
116 | elif contains_tokens(pattern) and contains_tokens(input):
117 | # Recurse:
118 | # try to match the first tokens of both pattern and input. The bindings
119 | # that result are used to match the remainder of both lists.
120 | return match_pattern(pattern[1:],
121 | input[1:],
122 | match_pattern(pattern[0], input[0], bindings))
123 | else:
124 | return False
125 |
126 |
127 | def match_segment(var, pattern, input, bindings, start=0):
128 | """
129 | Match the segment variable against the input.
130 |
131 | pattern and input should be lists of tokens.
132 |
133 | Looks for a substring of input that begins at start and is immediately
134 | followed by the first word in pattern. If such a substring exists,
135 | matching continues recursively and the resulting bindings are returned;
136 | otherwise returns False.
137 | """
138 |
139 | # If there are no words in pattern following var, we can just match var
140 | # to the remainder of the input.
141 | if not pattern:
142 | return match_variable(var, input, bindings)
143 |
144 | # Get the segment boundary word and look for the first occurrence in
145 | # the input starting from index start.
146 | word = pattern[0]
147 | try:
148 | pos = start + input[start:].index(word)
149 | except ValueError:
150 | # When the boundary word doesn't appear in the input, no match.
151 | return False
152 |
153 | # Match the located substring to the segment variable and recursively
154 | # pattern match using the resulting bindings.
155 | var_match = match_variable(var, input[:pos], dict(bindings))
156 | match = match_pattern(pattern, input[pos:], var_match)
157 |
158 | # If pattern matching fails with this substring, try a longer one.
159 | if not match:
160 | return match_segment(var, pattern, input, bindings, start + 1)
161 |
162 | return match
163 |
164 |
165 | def match_variable(var, replacement, bindings):
166 | """Bind the input to the variable and update the bindings."""
167 | binding = bindings.get(var)
168 | if not binding:
169 | # The variable isn't yet bound.
170 | bindings.update({var: replacement})
171 | return bindings
172 | if replacement == bindings[var]:
173 | # The variable is already bound to that input.
174 | return bindings
175 |
176 | # The variable is already bound, but not to that input--fail.
177 | return False
178 |
179 |
180 | ## Pattern matching utilities
181 |
182 | def contains_tokens(pattern):
183 | """Test if pattern is a list of subpatterns."""
184 | return type(pattern) is list and len(pattern) > 0
185 |
186 |
187 | def is_variable(pattern):
188 | """Test if pattern is a single variable."""
189 | return (type(pattern) is str
190 | and pattern[0] == '?'
191 | and len(pattern) > 1
192 | and pattern[1] != '*'
193 | and pattern[1] in string.letters
194 | and ' ' not in pattern)
195 |
196 |
197 | def is_segment(pattern):
198 | """Test if pattern begins with a segment variable."""
199 | return (type(pattern) is list
200 | and pattern
201 | and len(pattern[0]) > 2
202 | and pattern[0][0] == '?'
203 | and pattern[0][1] == '*'
204 | and pattern[0][2] in string.letters
205 | and ' ' not in pattern[0])
206 |
207 |
208 | ## Translating user input
209 |
210 | def replace(word, replacements):
211 | """Replace word with rep if (word, rep) occurs in replacements."""
212 | for old, new in replacements:
213 | if word == old:
214 | return new
215 | return word
216 |
217 |
218 | def switch_viewpoint(words):
219 | """Swap some common pronouns for interacting with a robot."""
220 | replacements = [('I', 'YOU'),
221 | ('YOU', 'I'),
222 | ('ME', 'YOU'),
223 | ('MY', 'YOUR'),
224 | ('AM', 'ARE'),
225 | ('ARE', 'AM')]
226 | return [replace(word, replacements) for word in words]
227 |
228 |
229 | def remove_punct(string):
230 | """Remove common punctuation marks."""
231 | if string.endswith('?'):
232 | string = string[:-1]
233 | return (string.replace(',', '')
234 | .replace('.', '')
235 | .replace(';', '')
236 | .replace('!', ''))
237 |
--------------------------------------------------------------------------------
/paip/tests/test_search.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import random
3 | import paip.search as search
4 |
5 |
6 | class Graph(object):
7 | def __init__(self, data, neighbors=None):
8 | self.data = data
9 | self.neighbors = neighbors or []
10 |
11 | def add_neighbor(self, node):
12 | self.neighbors.append(node)
13 |
14 | def __repr__(self):
15 | return str(self.data)
16 |
17 | def __eq__(self, other):
18 | return self.data == other.data
19 |
20 |
21 | class SearchTest(unittest.TestCase):
22 | def path_tracking_test(self, alg, start, finish, expected_path):
23 | path = []
24 | def done(node):
25 | return node == finish
26 | def successors(node):
27 | path.append(node)
28 | return node.neighbors
29 | found = alg(start, done, successors)
30 | self.assertEqual(found, finish)
31 | self.assertEqual(path, expected_path)
32 |
33 |
34 | # ----------------------------------------------------------------------------
35 | ## Tree search tests
36 |
37 | # A
38 | # B/ \C
39 | # D/ \E \F
40 | # /G |H \I \J \K
41 | # L/ \M \N \O
42 | # P/ QRST \UV
43 |
44 | a = Graph('a')
45 | b = Graph('b')
46 | c = Graph('c')
47 | d = Graph('d')
48 | e = Graph('e')
49 | f = Graph('f')
50 | g = Graph('g')
51 | h = Graph('h')
52 | i = Graph('i')
53 | j = Graph('j')
54 | k = Graph('k')
55 | l = Graph('l')
56 | m = Graph('m')
57 | n = Graph('n')
58 | o = Graph('o')
59 | p = Graph('p')
60 | q = Graph('q')
61 | r = Graph('r')
62 | s = Graph('s')
63 | t = Graph('t')
64 | u = Graph('u')
65 | v = Graph('v')
66 |
67 | a.neighbors = [b, c]
68 | b.neighbors = [d, e]
69 | c.neighbors = [f]
70 | e.neighbors = [g, h, i]
71 | f.neighbors = [j, k]
72 | g.neighbors = [l, m]
73 | h.neighbors = [n]
74 | j.neighbors = [o]
75 | l.neighbors = [p]
76 | m.neighbors = [q, r, s, t]
77 | n.neighbors = [u, v]
78 |
79 |
80 | class TreeSearchTest(SearchTest):
81 | def test_dfs(self):
82 | expected_path = [a, b, d, e, g, l, p, m, q, r]
83 | self.path_tracking_test(search.dfs, a, s, expected_path)
84 |
85 | def test_bfs(self):
86 | expected_path = [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r]
87 | self.path_tracking_test(search.bfs, a, s, expected_path)
88 |
89 | def test_best_first_search(self):
90 | def cost(n):
91 | return abs(ord(n.data) - ord(s.data))
92 | def alg(start, done, next):
93 | return search.best_first_search(start, done, next, cost)
94 | expected_path = [a, c, f, k, j, o, b, e, i, h, n, u, v, g, m]
95 | self.path_tracking_test(alg, a, s, expected_path)
96 |
97 | def test_beam_search(self):
98 | def cost(n):
99 | return -ord(n.data)
100 | def alg(start, done, next):
101 | return search.beam_search(start, done, next, cost, 3)
102 | expected_path = [a, c, f, k, j, o, b, e, i, h, n, v, u, g, m, t]
103 | self.path_tracking_test(alg, a, s, expected_path)
104 |
105 | def test_widening_search(self):
106 | def cost(n):
107 | return -ord(n.data)
108 | def alg(start, done, next):
109 | return search.widening_search(start, done, next, cost)
110 | expected_path = [
111 | a, c, f, k, # beam width 1
112 | a, c, f, k, j, o, # beam width 2
113 | a, c, f, k, j, o, b, e, i, h, n, v, u, g, m, t # beam width 3
114 | ]
115 | self.path_tracking_test(alg, a, s, expected_path)
116 |
117 |
118 | # ----------------------------------------------------------------------------
119 | ## Graph search tests
120 |
121 | g1 = Graph(1)
122 | g2 = Graph(2)
123 | g3 = Graph(3)
124 | g4 = Graph(4)
125 | g5 = Graph(5)
126 | g6 = Graph(6)
127 | g1.neighbors = [g4, g5, g6]
128 | g2.neighbors = [g1, g4]
129 | g3.neighbors = [g1, g2, g4]
130 | g4.neighbors = [g3]
131 | g5.neighbors = []
132 | g6.neighbors = [g2, g4]
133 |
134 |
135 | class GraphSearchTest(SearchTest):
136 | def test_bfs(self):
137 | expected_path = [g6, g2, g4, g1, g3]
138 | self.path_tracking_test(search.graph_search_bfs, g6, g5, expected_path)
139 |
140 | def test_dfs(self):
141 | expected_path = [g6, g2, g1]
142 | self.path_tracking_test(search.graph_search_dfs, g6, g5, expected_path)
143 |
144 |
145 | # ----------------------------------------------------------------------------
146 | ## Pathfinding utilities tests
147 |
148 | # NOTE: Same graph data as the graph search tests
149 |
150 | p1 = search.Path(g1, cost=1)
151 | p2 = search.Path(g2, cost=3)
152 | p3 = search.Path(g3, cost=5)
153 | p4 = search.Path(g4, cost=7)
154 |
155 | p2.prev_path = p1
156 | p3.prev_path = p2
157 | p4.prev_path = p3
158 |
159 | paths = [p1, p2, p3, p4]
160 |
161 | def comp(path1, path2):
162 | return path1.cost - path2.cost
163 |
164 | def cost(node1, node2):
165 | return abs(node1.data - node2.data)
166 |
167 | def collect_paths(paths):
168 | return [(p.cost, p.collect()) for p in paths]
169 |
170 |
171 | class PathTest(unittest.TestCase):
172 | def test_find_path(self):
173 | found = search.find_path(g3, paths)
174 | self.assertEqual(p3, found)
175 |
176 | def test_find_path_none(self):
177 | found = search.find_path(g5, paths)
178 | self.assertFalse(found)
179 |
180 | def test_insert_path_begin(self):
181 | look_in = [p2, p3, p4]
182 | search.insert_path(p1, look_in, comp)
183 | self.assertEqual(paths, look_in)
184 |
185 | def test_insert_path_middle(self):
186 | look_in = [p1, p2, p4]
187 | search.insert_path(p3, look_in, comp)
188 | self.assertEqual(paths, look_in)
189 |
190 | def test_insert_path_end(self):
191 | look_in = [p1, p2, p3]
192 | search.insert_path(p4, look_in, comp)
193 | self.assertEqual(paths, look_in)
194 |
195 | def test_collect_path(self):
196 | path = p4.collect()
197 | expected = [g1, g2, g3, g4]
198 | self.assertEqual(expected, path)
199 |
200 | def test_replace_if_better(self):
201 | look_in = list(paths)
202 | replace_in = []
203 | path = search.Path(g3, cost=4)
204 | y = search.replace_if_better(path, comp, look_in, replace_in)
205 | self.assertEqual([p1, p2, p4], look_in)
206 | self.assertEqual([path], replace_in)
207 | self.assertTrue(y)
208 |
209 | def test_replace_if_better_not_better(self):
210 | look_in = list(paths)
211 | replace_in = []
212 | path = search.Path(g3, cost=9)
213 | y = search.replace_if_better(path, comp, look_in, replace_in)
214 | self.assertEqual(paths, look_in)
215 | self.assertEqual([], replace_in)
216 | self.assertFalse(y)
217 |
218 | def test_replace_if_better_not_found(self):
219 | look_in = list(paths)
220 | replace_in = []
221 | path = search.Path(g5, cost=1)
222 | y = search.replace_if_better(path, comp, look_in, replace_in)
223 | self.assertEqual(paths, look_in)
224 | self.assertEqual([], replace_in)
225 | self.assertFalse(y)
226 |
227 | def test_replace_if_better_same_list(self):
228 | look_in = list(paths)
229 | path = search.Path(g3, cost=4)
230 | y = search.replace_if_better(path, comp, look_in, look_in)
231 | self.assertEqual([p1, p2, path, p4], look_in)
232 | self.assertTrue(y)
233 |
234 | def test_extend_path(self):
235 | start = search.Path(g3)
236 | to = [g1, g2, g4]
237 |
238 | path15 = search.Path(g5, search.Path(g1, None, 0), 4)
239 | path152 = search.Path(g2, path15, 7)
240 | path43 = search.Path(g3, search.Path(g4, None, 0), 1)
241 | path62 = search.Path(g2, search.Path(g6, None, 0), 4)
242 | path621 = search.Path(g1, path62, 5)
243 | path15 = search.Path(g5, search.Path(g1, None, 0), 4)
244 | path32 = search.Path(g2, search.Path(g3, None, 0), 1)
245 | path31 = search.Path(g1, search.Path(g3, None, 0), 2)
246 | path34 = search.Path(g4, search.Path(g3, None, 0), 1)
247 |
248 | current = [path43, path152]
249 | current_after = [path34, path32, path43, path31]
250 |
251 | old = [path15, path621]
252 | old_after = [path15]
253 |
254 | search.extend_path(start, to, current, old, cost, comp)
255 | self.assertEqual(collect_paths(old_after), collect_paths(old))
256 | self.assertEqual(collect_paths(current_after), collect_paths(current))
257 |
258 |
259 | # ----------------------------------------------------------------------------
260 | ## A* tests
261 |
262 | # NOTE: Same graph data as the graph search tests
263 |
264 | class AStarTest(unittest.TestCase):
265 | def a_star_test(self, a, b, heuristic, expected_path, expected_cost):
266 | finished = lambda node: node is b
267 | next = lambda node: node.neighbors
268 | path = search.a_star([search.Path(a)], finished, next, cost, heuristic)
269 | self.assertEqual(expected_path, path.collect())
270 | self.assertEqual(expected_cost, path.cost)
271 | self.assertEqual(b, path.state)
272 |
273 | def test_dijkstra(self):
274 | h = lambda node: 0
275 | expected = [g6, g4, g3, g1, g5]
276 | self.a_star_test(g6, g5, h, expected, 9)
277 |
278 | def test_a_star(self):
279 | h = lambda node: abs(node.data - g5.data)
280 | expected = [g6, g4, g3, g1, g5]
281 | self.a_star_test(g6, g5, h, expected, 9)
282 |
--------------------------------------------------------------------------------
/paip/examples/search/gps.py:
--------------------------------------------------------------------------------
1 | from paip.search import beam_search
2 |
3 | ## GPS implemented as search
4 |
5 | def successors(states, operators):
6 | ret = []
7 | for op in applicable_ops(states, operators):
8 | ret.append([s for s in states if s not in op['delete']] + op['add'])
9 | return ret
10 |
11 |
12 | def applicable_ops(states, ops):
13 | states = set(states)
14 | return [op for op in ops if set(op['preconds']) <= states]
15 |
16 |
17 | def gps(initial_states, goal_states, operators, beam_width=10):
18 | prefix = 'Executing '
19 | for operator in operators:
20 | operator['add'].append(prefix + operator['action'])
21 |
22 | def get_successors(states):
23 | return successors(states, operators)
24 |
25 | def goal_reached(states):
26 | for goal in goal_states:
27 | if goal not in states:
28 | return False
29 | return True
30 |
31 | def cost(states):
32 | sum = len([s for s in states if s.startswith(prefix)])
33 | sum += len([g for g in goal_states if g not in states])
34 | return sum
35 |
36 | final = beam_search(initial_states, goal_reached,
37 | get_successors, cost, beam_width)
38 | return [state for state in final if state.startswith(prefix)]
39 |
40 |
41 | ## Example problem definition
42 |
43 | problem = {
44 | "start": ["space on a", "a on b", "b on c", "c on table", "space on table"],
45 | "finish": ["space on c", "c on b", "b on a", "a on table", "space on table"],
46 | "ops": [
47 | {
48 | "action": "move a from b to c",
49 | "preconds": [
50 | "space on a",
51 | "space on c",
52 | "a on b"
53 | ],
54 | "add": [
55 | "a on c",
56 | "space on b"
57 | ],
58 | "delete": [
59 | "a on b",
60 | "space on c"
61 | ]
62 | },
63 | {
64 | "action": "move a from table to b",
65 | "preconds": [
66 | "space on a",
67 | "space on b",
68 | "a on table"
69 | ],
70 | "add": [
71 | "a on b"
72 | ],
73 | "delete": [
74 | "a on table",
75 | "space on b"
76 | ]
77 | },
78 | {
79 | "action": "move a from b to table",
80 | "preconds": [
81 | "space on a",
82 | "space on table",
83 | "a on b"
84 | ],
85 | "add": [
86 | "a on table",
87 | "space on b"
88 | ],
89 | "delete": [
90 | "a on b"
91 | ]
92 | },
93 | {
94 | "action": "move a from c to b",
95 | "preconds": [
96 | "space on a",
97 | "space on b",
98 | "a on c"
99 | ],
100 | "add": [
101 | "a on b",
102 | "space on c"
103 | ],
104 | "delete": [
105 | "a on c",
106 | "space on b"
107 | ]
108 | },
109 | {
110 | "action": "move a from table to c",
111 | "preconds": [
112 | "space on a",
113 | "space on c",
114 | "a on table"
115 | ],
116 | "add": [
117 | "a on c"
118 | ],
119 | "delete": [
120 | "a on table",
121 | "space on c"
122 | ]
123 | },
124 | {
125 | "action": "move a from c to table",
126 | "preconds": [
127 | "space on a",
128 | "space on table",
129 | "a on c"
130 | ],
131 | "add": [
132 | "a on table",
133 | "space on c"
134 | ],
135 | "delete": [
136 | "a on c"
137 | ]
138 | },
139 | {
140 | "action": "move b from a to c",
141 | "preconds": [
142 | "space on b",
143 | "space on c",
144 | "b on a"
145 | ],
146 | "add": [
147 | "b on c",
148 | "space on a"
149 | ],
150 | "delete": [
151 | "b on a",
152 | "space on c"
153 | ]
154 | },
155 | {
156 | "action": "move b from table to a",
157 | "preconds": [
158 | "space on b",
159 | "space on a",
160 | "b on table"
161 | ],
162 | "add": [
163 | "b on a"
164 | ],
165 | "delete": [
166 | "b on table",
167 | "space on a"
168 | ]
169 | },
170 | {
171 | "action": "move b from a to table",
172 | "preconds": [
173 | "space on b",
174 | "space on table",
175 | "b on a"
176 | ],
177 | "add": [
178 | "b on table",
179 | "space on a"
180 | ],
181 | "delete": [
182 | "b on a"
183 | ]
184 | },
185 | {
186 | "action": "move b from c to a",
187 | "preconds": [
188 | "space on b",
189 | "space on a",
190 | "b on c"
191 | ],
192 | "add": [
193 | "b on a",
194 | "space on c"
195 | ],
196 | "delete": [
197 | "b on c",
198 | "space on a"
199 | ]
200 | },
201 | {
202 | "action": "move b from table to c",
203 | "preconds": [
204 | "space on b",
205 | "space on c",
206 | "b on table"
207 | ],
208 | "add": [
209 | "b on c"
210 | ],
211 | "delete": [
212 | "b on table",
213 | "space on c"
214 | ]
215 | },
216 | {
217 | "action": "move b from c to table",
218 | "preconds": [
219 | "space on b",
220 | "space on table",
221 | "b on c"
222 | ],
223 | "add": [
224 | "b on table",
225 | "space on c"
226 | ],
227 | "delete": [
228 | "b on c"
229 | ]
230 | },
231 | {
232 | "action": "move c from a to b",
233 | "preconds": [
234 | "space on c",
235 | "space on b",
236 | "c on a"
237 | ],
238 | "add": [
239 | "c on b",
240 | "space on a"
241 | ],
242 | "delete": [
243 | "c on a",
244 | "space on b"
245 | ]
246 | },
247 | {
248 | "action": "move c from table to a",
249 | "preconds": [
250 | "space on c",
251 | "space on a",
252 | "c on table"
253 | ],
254 | "add": [
255 | "c on a"
256 | ],
257 | "delete": [
258 | "c on table",
259 | "space on a"
260 | ]
261 | },
262 | {
263 | "action": "move c from a to table",
264 | "preconds": [
265 | "space on c",
266 | "space on table",
267 | "c on a"
268 | ],
269 | "add": [
270 | "c on table",
271 | "space on a"
272 | ],
273 | "delete": [
274 | "c on a"
275 | ]
276 | },
277 | {
278 | "action": "move c from b to a",
279 | "preconds": [
280 | "space on c",
281 | "space on a",
282 | "c on b"
283 | ],
284 | "add": [
285 | "c on a",
286 | "space on b"
287 | ],
288 | "delete": [
289 | "c on b",
290 | "space on a"
291 | ]
292 | },
293 | {
294 | "action": "move c from table to b",
295 | "preconds": [
296 | "space on c",
297 | "space on b",
298 | "c on table"
299 | ],
300 | "add": [
301 | "c on b"
302 | ],
303 | "delete": [
304 | "c on table",
305 | "space on b"
306 | ]
307 | },
308 | {
309 | "action": "move c from b to table",
310 | "preconds": [
311 | "space on c",
312 | "space on table",
313 | "c on b"
314 | ],
315 | "add": [
316 | "c on table",
317 | "space on b"
318 | ],
319 | "delete": [
320 | "c on b"
321 | ]
322 | }
323 | ]
324 | }
325 |
326 |
327 | from paip import search
328 |
329 |
330 | def main():
331 | start = problem['start']
332 | finish = problem['finish']
333 | ops = problem['ops']
334 | for action in gps(start, finish, ops):
335 | print action
336 |
337 |
338 | if __name__ == '__main__':
339 | main()
340 |
--------------------------------------------------------------------------------
/paip/tests/test_othello.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import random
3 | from paip.othello import *
4 |
5 | class MoveTests(unittest.TestCase):
6 | def setUp(self):
7 | b = initial_board()
8 | # fig. 18.2a from PAIP p.598
9 | b[33:35] = [WHITE, BLACK]
10 | b[43:46] = [WHITE, WHITE, BLACK]
11 | b[52:57] = [WHITE, WHITE, WHITE, WHITE, BLACK]
12 | b[64:66] = [BLACK, WHITE]
13 | self.board = b
14 |
15 | def test_is_valid(self):
16 | self.assertTrue(is_valid(27))
17 | self.assertFalse(is_valid(30))
18 | self.assertFalse(is_valid(49))
19 | self.assertFalse(is_valid(-2))
20 | self.assertFalse(is_valid(124))
21 | self.assertFalse(is_valid('foo'))
22 |
23 | def test_find_bracketing_piece_none(self):
24 | square = 63
25 | self.assertEqual(None, find_bracket(63, WHITE, self.board, UP))
26 | self.assertEqual(None, find_bracket(63, WHITE, self.board, DOWN_RIGHT))
27 | self.assertEqual(None, find_bracket(63, WHITE, self.board, DOWN))
28 | self.assertEqual(None, find_bracket(63, WHITE, self.board, DOWN_LEFT))
29 | self.assertEqual(None, find_bracket(63, WHITE, self.board, LEFT))
30 | self.assertEqual(None, find_bracket(63, WHITE, self.board, UP_LEFT))
31 |
32 | def test_find_bracket(self):
33 | square = 42
34 | self.assertEqual(45, find_bracket(42, BLACK, self.board, RIGHT))
35 | self.assertEqual(64, find_bracket(42, BLACK, self.board, DOWN_RIGHT))
36 |
37 | def test_is_legal_false_not_empty(self):
38 | self.assertFalse(is_legal(64, WHITE, self.board))
39 |
40 | def test_is_legal_false_no_bracket(self):
41 | self.assertFalse(is_legal(42, WHITE, self.board))
42 |
43 | def test_is_legal(self):
44 | self.assertTrue(is_legal(42, BLACK, self.board))
45 |
46 | def test_make_flips_none(self):
47 | b1, b2 = self.board, list(self.board)
48 | make_flips(42, WHITE, b2, RIGHT)
49 | self.assertEqual(b1, b2)
50 |
51 | def test_make_flips(self):
52 | b1, b2 = self.board, list(self.board)
53 | make_flips(42, BLACK, b2, RIGHT)
54 | b1[43:45] = [BLACK, BLACK]
55 | self.assertEqual(b1, b2)
56 |
57 | def test_make_move(self):
58 | b1, b2 = self.board, list(self.board)
59 | make_move(42, BLACK, b2)
60 | b1[42] = BLACK
61 | b1[43:45] = [BLACK, BLACK]
62 | b1[53] = BLACK
63 | self.assertEqual(b1, b2)
64 |
65 |
66 | class GameTests(unittest.TestCase):
67 | def setUp(self):
68 | self.board = initial_board()
69 | for sq in squares():
70 | self.board[sq] = WHITE
71 | self.board[11] = EMPTY
72 | self.board[88] = BLACK
73 |
74 | def test_any_legal_move_false(self):
75 | self.assertFalse(any_legal_move(WHITE, self.board))
76 |
77 | def test_any_legal_move(self):
78 | self.assertTrue(any_legal_move(BLACK, self.board))
79 |
80 | def test_next_player_repeat(self):
81 | self.assertEqual(BLACK, next_player(self.board, BLACK))
82 |
83 | def test_next_player_none(self):
84 | self.board[11] = WHITE
85 | self.assertEqual(None, next_player(self.board, BLACK))
86 |
87 | def test_next_player(self):
88 | self.assertEqual(BLACK, next_player(self.board, WHITE))
89 |
90 | def test_get_move_invalid(self):
91 | strategy = lambda player, board: -23
92 | self.assertRaises(IllegalMoveError, get_move, strategy, BLACK, self.board)
93 |
94 | def test_get_move_illegal(self):
95 | strategy = lambda player, board: 11
96 | self.assertRaises(IllegalMoveError, get_move, strategy, WHITE, self.board)
97 |
98 | def test_get_move(self):
99 | strategy = lambda player, board: 11
100 | self.assertEqual(11, get_move(strategy, BLACK, self.board))
101 |
102 | def test_score(self):
103 | make_move(11, BLACK, self.board)
104 | self.assertEqual(8 - 56, score(BLACK, self.board))
105 |
106 | def test_play(self):
107 | player_accesses = []
108 | def random_strategy(player, board):
109 | player_accesses.append(player)
110 | legal = [sq for sq in squares() if is_legal(sq, player, board)]
111 | return random.choice(legal)
112 | board, score = play(random_strategy, random_strategy)
113 |
114 | # check that no moves remain
115 | self.assertFalse(any_legal_move(BLACK, board))
116 | self.assertFalse(any_legal_move(WHITE, board))
117 |
118 | # check that both players had a turn
119 | self.assertTrue(BLACK in player_accesses)
120 | self.assertTrue(WHITE in player_accesses)
121 |
122 |
123 | class StrategyTests(unittest.TestCase):
124 | def setUp(self):
125 | b = initial_board()
126 | # 1 2 3 4 5 6 7 8
127 | # 1 . . . . . . . .
128 | # 2 . . . . . . . .
129 | # 3 . . o @ . o . .
130 | # 4 . . o o @ @ . .
131 | # 5 . o o o o @ . .
132 | # 6 . . . @ o . . .
133 | # 7 . . . . . . . .
134 | # 8 . . . . . . . .
135 | b[33:37] = [WHITE, BLACK, EMPTY, WHITE]
136 | b[43:47] = [WHITE, WHITE, BLACK, BLACK]
137 | b[52:57] = [WHITE, WHITE, WHITE, WHITE, BLACK]
138 | b[64:66] = [BLACK, WHITE]
139 | self.board = b
140 |
141 | def test_maximizer(self):
142 | self.assertEqual(51, maximizer(score)(BLACK, self.board))
143 | self.assertEqual(47, maximizer(score)(WHITE, self.board))
144 |
145 | def test_weighted_score(self):
146 | score = weighted_score(BLACK, self.board)
147 | expected = 5 * 3 - (6 * 3 - 5 + 2 * 15)
148 | self.assertEqual(expected, score)
149 | self.assertEqual(-score, weighted_score(WHITE, self.board))
150 |
151 | def test_final_value(self):
152 | self.assertEqual(MIN_VALUE, final_value(BLACK, self.board))
153 | self.assertEqual(MAX_VALUE, final_value(WHITE, self.board))
154 |
155 | def test_minimax_leaf(self):
156 | val = score(BLACK, self.board)
157 | self.assertEqual((val, None), minimax(BLACK, self.board, 0, score))
158 | self.assertEqual((-val, None), minimax(WHITE, self.board, 0, score))
159 |
160 | def test_minimax_game_over(self):
161 | result, outcome = play(random_strategy, random_strategy)
162 | if outcome == 0:
163 | val_black, val_white = 0, 0
164 | else:
165 | val_black = MAX_VALUE if outcome > 0 else MIN_VALUE
166 | val_white = -val_black
167 | self.assertEqual((val_black, None), minimax(BLACK, result, 20, score))
168 | self.assertEqual((val_white, None), minimax(WHITE, result, 20, score))
169 |
170 | def test_minimax_pass(self):
171 | # remove all black pieces so black has no moves
172 | for sq in squares():
173 | if self.board[sq] == BLACK:
174 | self.board[sq] = EMPTY
175 | # result:
176 | # 1 2 3 4 5 6 7 8
177 | # 1 . . . . . . . .
178 | # 2 . . . . . . . .
179 | # 3 . . o . . o . .
180 | # 4 . . o o . . . .
181 | # 5 . o o o o . . .
182 | # 6 . . . . o . . .
183 | # 7 . . . . . . . .
184 | # 8 . . . . . . . .
185 |
186 | # leave one move for white
187 | self.board[57:59] = [BLACK, WHITE]
188 | # result:
189 | # 1 2 3 4 5 6 7 8
190 | # 1 . . . . . . . .
191 | # 2 . . . . . . . .
192 | # 3 . . o . . o . .
193 | # 4 . . o o . . . .
194 | # 5 . o o o o . @ o
195 | # 6 . . . . o . . .
196 | # 7 . . . . . . . .
197 | # 8 . . . . . . . .
198 |
199 | accesses = []
200 | def evaluate(player, board):
201 | accesses.append(player)
202 | return score(player, board)
203 | self.assertEqual(0, len(accesses))
204 | self.assertEqual((MIN_VALUE, None), minimax(BLACK, self.board, 20, evaluate))
205 |
206 | def test_minimax(self):
207 | board = initial_board()
208 | for sq in squares():
209 | board[sq] = EMPTY
210 |
211 | # 1 2 3 4 5 6 7 8
212 | # 1 . . . . . . . .
213 | # 2 . . . . . . . .
214 | # 3 . . . . . . . .
215 | # 4 . . . . . . . .
216 | # 5 . . . . . . . .
217 | # 6 . . . . . . . .
218 | # 7 . @ @ @ o @ . .
219 | # 8 . . . . . . . .
220 | board[72:77] = [BLACK, BLACK, BLACK, WHITE, BLACK]
221 | accesses = []
222 | def evaluate(player, board):
223 | accesses.append(player)
224 | return score(player, board)
225 | self.assertEqual((MAX_VALUE, 71), minimax(WHITE, board, 20, evaluate))
226 | self.assertEqual(0, len(accesses))
227 |
228 | def test_alphabeta(self):
229 | board = initial_board()
230 | for sq in squares():
231 | board[sq] = EMPTY
232 |
233 | # 1 2 3 4 5 6 7 8
234 | # 1 . @ o . . . . .
235 | # 2 . . . . . . . .
236 | # 3 . @ o o o . . .
237 | # 4 . . . . . . . .
238 | # 5 . @ o o o o o .
239 | # 6 . . . . . . . .
240 | # 7 . @ o o . . . .
241 | # 8 . . . . . . . .
242 | board[12:14] = [BLACK, WHITE]
243 | board[32:35] = [BLACK, WHITE, WHITE, WHITE]
244 | board[52:56] = [BLACK, WHITE, WHITE, WHITE, WHITE, WHITE]
245 | board[72:77] = [BLACK, WHITE, WHITE]
246 |
247 | # 1. 14, value -4, below alpha, ignore
248 | # 2. 36, value 0, above alpha, update alpha
249 | # 3. 58, value 4, above alpha, update alpha
250 | # 2. 75, value -2, alpha above beta, ignore and break
251 | expected_values = [4, 0, -4]
252 |
253 | values = []
254 | def evaluate(player, board):
255 | val = score(player, board)
256 | values.append(val)
257 | return val
258 | self.assertEqual((4, 58), alphabeta(BLACK, board, -1, 3, 1, evaluate))
259 | self.assertEqual(expected_values, values)
260 |
--------------------------------------------------------------------------------
/paip/abandoned/logic.lisp:
--------------------------------------------------------------------------------
1 | ; Logic programming
2 |
3 | ;;; Overview:
4 | ;;; - single uniform database--fewer data structures
5 | ;;; - facts are represented as relations, can be used to deduce new facts
6 | ;;; - variables are "logic variables", bound by "unification", never change value
7 | ;;; - programmer gives constraints without specifying how to do the evaluation
8 | ;;; - "automatic backtracking" allows the programmer to specify multiple constraints,
9 | ;;; and Prolog will try to find facts that satisfy all of them
10 |
11 | ;; Idea 1: Uniform database
12 |
13 | ;;; Data takes the form of a database of assertions, called "clauses", which come
14 | ;;; in two types: facts and rules.
15 | ;;; Facts state a relationship that holds between two objects
16 | ;;; Rules state contingent facts (predicates?)
17 |
18 | ;;; Two ways to procedurally interpret declarative forms:
19 | ;;; - backward-chaining: reasoning backwards from the goal to the premises
20 | ;;; - forward-chaining: reasoning forwards from the premises to a conclusion
21 |
22 | ;;; Prolog does backward chaining only.
23 |
24 | ;; Idea 2: Unification of logic variables
25 |
26 | ;;; Method for stating that a variable is equal to an expression or another variable.
27 |
28 | (defconstant fail nil "Indicates pat-match failure")
29 |
30 | (defconstant no-bindings '((t . t)))
31 |
32 | (defparameter *occurs-check* t "Should we do the occurs check?")
33 |
34 | (defun variable-p (x)
35 | "Is x a variable (a symbol beginning with ?)?"
36 | (and (symbolp x) (equal (char (symbol-name x) 0) #\?)))
37 |
38 | (defun get-binding (var bindings)
39 | "Find a (variable . value) pair in a binding list."
40 | (assoc var bindings))
41 |
42 | (defun binding-val (binding)
43 | "Get the value part of a single binding."
44 | (cdr binding))
45 |
46 | (defun lookup (var bindings)
47 | "Get the value part (for var) from a binding list."
48 | (binding-val (get-binding var bindings)))
49 |
50 | (defun extend-bindings (var val bindings)
51 | "Add a (var . value) pair to a binding list."
52 | (cons (cons var val)
53 | (if (equal bindings no-bindings)
54 | nil
55 | bindings)))
56 |
57 | (defun match-variable (var input bindings)
58 | "Does VAR match input? Uses (or updates) and returns bindings."
59 | (let ((binding (get-binding var bindings)))
60 | (cond ((not binding) (extend-bindings var input bindings))
61 | ((equal input (binding-val binding)) bindings)
62 | (t fail))))
63 |
64 | (defun unify (x y &optional (bindings no-bindings))
65 | "See if x and y match with given bindings."
66 | (cond ((eq bindings fail) fail)
67 | ((eql x y) bindings)
68 | ((variable-p x) (unify-variable x y bindings))
69 | ((variable-p y) (unify-variable y x bindings))
70 | ((and (consp x) (consp y))
71 | (unify (rest x) (rest y)
72 | (unify (first x) (first y) bindings)))
73 | (t fail)))
74 |
75 | (defun unify-variable (var x bindings)
76 | "Unify var with x, using (and maybe extending) bindings."
77 | (cond ((get-binding var bindings)
78 | (unify (lookup var bindings) x bindings))
79 | ((and (variable-p x) (get-binding x bindings))
80 | (unify var (lookup x bindings) bindings))
81 | ((and *occurs-check* (occurs-check var x bindings))
82 | fail)
83 | (t (extend-bindings var x bindings))))
84 |
85 | (defun occurs-check (var x bindings)
86 | "Does var occur anywhere inside x? (infinite circular unification!)"
87 | (cond ((eq var x) t)
88 | ((and (variable-p x) (get-binding x bindings))
89 | (occurs-check var (lookup x bindings) bindings))
90 | ((consp x) (or (occurs-check var (first x) bindings)
91 | (occurs-check var (rest x) bindings)))
92 | (t nil)))
93 |
94 | (defun subst-bindings (bindings x)
95 | "Substitute the value of variables in bindings into x, taking recursively
96 | bound variables into account."
97 | (cond ((eq bindings fail) fail)
98 | ((eq bindings no-bindings) x)
99 | ((and (variable-p x) (get-binding x bindings))
100 | (subst-bindings bindings (lookup x bindings)))
101 | ((atom x) x)
102 | (t (cons (subst-bindings bindings (car x))
103 | (subst-bindings bindings (cdr x))))))
104 |
105 | (defun unifier (x y)
106 | "Return something that unifies with both x and y (or fail)."
107 | (subst-bindings (unify x y) x))
108 |
109 | ;; represent clauses as (head . body)
110 | (defun clause-head (clause) (first clause))
111 | (defun clause-body (clause) (rest clause))
112 |
113 | ;; indexing--we interpret a clause as: to prove the head, prove the body.
114 | ;; so index on heads.
115 |
116 | ;; clauses are stored on the predicate's plist
117 | ;; DC: this is stupid. we can't reach the property list, it's some magical cloud thing
118 | (defun get-clauses (pred) (get pred 'clauses))
119 | (defun predicate (relation) (first relation))
120 |
121 | (defvar *db-predicates* nil
122 | "A list of all predicates stored in the database.")
123 |
124 | (defmacro <- (&rest clause)
125 | `(add-clause ',clause))
126 |
127 | (defun add-clause (clause)
128 | "Add a clause to the database, indexed by head's predicate"
129 | ;; predicate must be a non-variable symbol.
130 | (let ((pred (predicate (clause-head clause))))
131 | (assert (and (symbolp pred) (not (variable-p pred))))
132 | (pushnew pred *db-predicates*)
133 | (setf (get pred 'clauses)
134 | (nconc (get-clauses pred) (list clause)))
135 | pred))
136 |
137 | (defun clear-db ()
138 | "Remove all clauses (for all predicates) from the data base."
139 | (mapc #'clear-predicate *db-predicates*))
140 |
141 | (defun clear-predicate (predicate)
142 | "Remove the clauses for a single predicate."
143 | (setf (get predicate 'clauses) nil))
144 |
145 | ;; (defun prove (goal bindings)
146 | ;; "Return a list of possible solutions to a goal."
147 | ;; ;; To prove goal, first find candidate clauses.
148 | ;; ;; For each candidate, check if goal unifies with the head of the clause.
149 | ;; ;; If so, try to prove all the goals in the body of the clause.
150 | ;; ;; --> for facts, there are no goals in the body, which means success.
151 | ;; ;; --> for rules, goals in body are proved one at a time, propagating bindings.
152 | ;; (mapcan #'(lambda (clause)
153 | ;; (let ((new-clause (rename-variables clause)))
154 | ;; (prove-all (clause-body new-clause)
155 | ;; (unify goal (clause-head new-clause) bindings))))
156 | ;; (get-clauses (predicate goal))))
157 |
158 | ;; (defun prove-all (goals bindings)
159 | ;; "Return a list of solutions to the conjunction of goals."
160 | ;; (cond ((eq bindings fail) fail)
161 | ;; ((null goals) (list bindings))
162 | ;; (t (mapcan #'(lambda (goal1-solution)
163 | ;; (prove-all (rest goals) goal1-solution))
164 | ;; (prove (first goals) bindings)))))
165 |
166 | (defun rename-variables (x)
167 | "Replace all variables in x with new ones."
168 | (sublis (mapcar #'(lambda (var) (cons var (gensym (string var))))
169 | (variables-in x))
170 | x))
171 |
172 | (defun variables-in (exp)
173 | "Return a list of all the variables in EXP."
174 | (unique-find-anywhere-if #'variable-p exp))
175 |
176 | (defun unique-find-anywhere-if (predicate tree &optional found-so-far)
177 | "Return a list of leaves of tree satisfying predicate, with duplicates removed."
178 | (if (atom tree)
179 | (if (funcall predicate tree)
180 | (adjoin tree found-so-far)
181 | found-so-far)
182 | (unique-find-anywhere-if predicate
183 | (first tree)
184 | (unique-find-anywhere-if predicate
185 | (rest tree)
186 | found-so-far))))
187 |
188 | (defmacro ?- (&rest goals) `(top-level-prove ',goals))
189 |
190 | ;; (defun top-level-prove (goals)
191 | ;; "Prove the goals, and print variables readably."
192 | ;; (show-prolog-solutions
193 | ;; (variables-in goals)
194 | ;; (prove-all goals no-bindings)))
195 |
196 | (defun show-prolog-solutions (vars solutions)
197 | "Print the variables in each of the solutions."
198 | (if (null solutions)
199 | (format t "~&No.")
200 | (mapc #'(lambda (solution) (show-prolog-vars vars solution))
201 | solutions))
202 | (values))
203 |
204 | ;; (defun show-prolog-vars (vars bindings)
205 | ;; "Print each variable with its binding."
206 | ;; (if (null vars)
207 | ;; (format t "~&Yes")
208 | ;; (dolist (var vars)
209 | ;; (format t "~&~a = ~a" var
210 | ;; (subst-bindings bindings var))))
211 | ;; (princ ";"))
212 |
213 | ;; Idea 3: automatic backtracking (redefining prove and prove-all)
214 |
215 | (defun prove-all (goals bindings)
216 | "Find a solution to the conjunction of goals."
217 | (cond ((eq bindings fail) fail)
218 | ((null goals) bindings)
219 | (t (prove (first goals) bindings (rest goals)))))
220 |
221 | (defun prove (goal bindings other-goals)
222 | "Return a list of possible solutions to goal."
223 | (let ((clauses (get-clauses (predicate goal))))
224 | (if (listp clauses)
225 | (some
226 | #'(lambda (clause)
227 | (let ((new-clause (rename-variables clause)))
228 | (prove-all
229 | (append (clause-body new-clause) other-goals)
230 | (unify goal (clause-head new-clause) bindings))))
231 | clauses)
232 | ;; If clauses isn't a list, it might be a primitive function to call (atom)
233 | (funcall clauses (rest goal) bindings other-goals))))
234 |
235 | (defun top-level-prove (goals)
236 | (prove-all `(,@goals (show-prolog-vars ,@(variables-in goals)))
237 | no-bindings)
238 | (format t "~&No.")
239 | (values))
240 |
241 | (defun show-prolog-vars (vars bindings other-goals)
242 | "Print each variable with its binding.
243 | Then ask the user if more solutions are desired."
244 | (if (null vars)
245 | (format t "~&Yes")
246 | (dolist (var vars)
247 | (format t "~&~a = ~a" var
248 | (subst-bindings bindings var))))
249 | (if (continue-p)
250 | fail
251 | (prove-all other-goals bindings)))
252 |
253 | (setf (get 'show-prolog-vars 'clauses) 'show-prolog-vars)
254 |
255 | (defun continue-p ()
256 | "Ask user if we should continue looking for solutions."
257 | (case (read-char)
258 | (#\; t)
259 | (#\. nil)
260 | (#\newline (continue-p))
261 | (otherwise
262 | (format t " Type ; to see more or . to stop")
263 | (continue-p))))
264 |
265 | (defmacro <- (&rest clause)
266 | "Add a clause to the database."
267 | `(add-clause ',(replace-?-vars clause)))
268 |
269 | (defmacro ?- (&rest goals)
270 | "Make a query and print answers."
271 | `(top-level-prove ',(replace-?-vars goals)))
272 |
273 | (defun replace-?-vars (exp)
274 | "Replace any ? within exp with a var of the form ?123."
275 | (cond ((eq exp '?) (gensym "?"))
276 | ((atom exp) exp)
277 | (t (cons (replace-?-vars (first exp))
278 | (replace-?-vars (rest exp))))))
279 |
--------------------------------------------------------------------------------
/prolog.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 |
3 | import argparse
4 | import logging
5 | import sys
6 |
7 | from paip import logic
8 |
9 |
10 | ## Parser and REPL
11 |
12 | # QUESTION = "?"
13 | # DEFN_BEGIN = "<-"
14 | # QUERY_BEGIN = QUESTION "-"
15 | # NUM = (-|+)?[0-9]+("."[0-9]+)?
16 | # IDENT: [a-zA-Z][a-zA-Z0-9_]*
17 | # WHEN = ":-"
18 | # LPAREN = "("
19 | # RPAREN = ")"
20 | # COMMA = ","
21 |
22 | # command: EOF | query | defn
23 | # query: QUERY_BEGIN relation
24 | # defn: DEFN_BEGIN relation (WHEN relation_list)?
25 | # relation_list = relation [COMMA relation]*
26 | # relation: IDENT LPAREN term [COMMA term]* RPAREN
27 | # term: relation | var | atom
28 | # atom: NUM | IDENT
29 | # var: QUESTION IDENT
30 |
31 |
32 | class ParseError(Exception):
33 | def __init__(self, err):
34 | self.err = err
35 |
36 | def __str__(self):
37 | return 'Parse error: %s' % self.err
38 |
39 |
40 | class Parser(object):
41 | k = 2
42 |
43 | def __init__(self, lexer):
44 | self.lexer = lexer
45 | self.lookahead = []
46 | for i in xrange(Parser.k):
47 | self.lookahead.append(lexer.next())
48 |
49 | def la(self, i):
50 | return self.lookahead[i-1]
51 |
52 | def match(self, exp_tt):
53 | tt, tok = self.la(1)
54 | if tt != exp_tt:
55 | raise ParseError('Expected %s, got %s' % (exp_tt, tt))
56 | self.lookahead.pop(0)
57 | self.lookahead.append(self.lexer.next())
58 | return tok
59 |
60 | def command(self):
61 | tt, tok = self.la(1)
62 | if tt == EOF:
63 | return
64 | if tt == QUERY_BEGIN:
65 | return self.query()
66 | elif tt == DEFN_BEGIN:
67 | return self.defn()
68 | raise ParseError('Unknown command: %s' % tok)
69 |
70 | def query(self):
71 | self.match(QUERY_BEGIN)
72 | return self.relation()
73 |
74 | def defn(self):
75 | self.match(DEFN_BEGIN)
76 | head = self.relation()
77 | tt, tok = self.la(1)
78 | if tt == WHEN:
79 | self.match(WHEN)
80 | return logic.Clause(head, self.relation_list())
81 | return logic.Clause(head)
82 |
83 | def relation_list(self):
84 | rels = [self.relation()]
85 | tt, tok = self.la(1)
86 | while tt == COMMA:
87 | self.match(COMMA)
88 | rels.append(self.relation())
89 | tt, tok = self.la(1)
90 | return rels
91 |
92 | def relation(self):
93 | pred = self.match(IDENT)
94 | body = []
95 | self.match(LPAREN)
96 | body.append(self.term())
97 | tt, tok = self.la(1)
98 | while tt == COMMA:
99 | self.match(COMMA)
100 | body.append(self.term())
101 | tt, tok = self.la(1)
102 | self.match(RPAREN)
103 | return logic.Relation(pred, body)
104 |
105 | def term(self):
106 | tt, tok = self.la(1)
107 | if tt == QUESTION:
108 | return self.var()
109 | elif tt == NUM:
110 | return self.atom()
111 | elif tt == IDENT:
112 | tt2, tok2 = self.la(2)
113 | if tt2 == LPAREN:
114 | return self.relation()
115 | else:
116 | return self.atom()
117 | else:
118 | raise ParseError('Unknown term lookahead: %s' % tok)
119 |
120 | def var(self):
121 | self.match(QUESTION)
122 | return logic.Var(self.match(IDENT))
123 |
124 | def atom(self):
125 | tt, tok = self.la(1)
126 | if tt == NUM:
127 | return logic.Atom(self.match(NUM))
128 | elif tt == IDENT:
129 | return logic.Atom(self.match(IDENT))
130 | else:
131 | raise ParseError('Unknown atom: %s' % tok)
132 |
133 |
134 | class TokenError(Exception):
135 | def __init__(self, err):
136 | self.err = err
137 |
138 | def __str__(self):
139 | return 'Token error: %s' % self.err
140 |
141 |
142 | LPAREN = 'LPAREN'
143 | RPAREN = 'RPAREN'
144 | COMMA = 'COMMA'
145 | QUESTION = 'QUESTION'
146 | DEFN_BEGIN = 'DEFN_BEGIN'
147 | QUERY_BEGIN = 'QUERY_BEGIN'
148 | NUM = 'NUM'
149 | IDENT = 'IDENT'
150 | WHEN = 'WHEN'
151 | EOF = 'EOF'
152 |
153 |
154 | class Lexer(object):
155 | def __init__(self, line):
156 | self.line = line
157 | self.pos = 0
158 | self.ch = line[self.pos]
159 |
160 | def eat(self):
161 | ret = self.ch
162 | self.pos += 1
163 | if self.pos >= len(self.line):
164 | self.ch = EOF
165 | else:
166 | self.ch = self.line[self.pos]
167 | return ret
168 |
169 | def match(self, exp):
170 | if self.ch != exp:
171 | raise TokenError('expected %s' % exp)
172 | self.eat()
173 |
174 | def expect(self, is_type):
175 | if not is_type():
176 | raise TokenError('expected type %s' % repr(is_type))
177 |
178 | def is_ws(self):
179 | return self.ch in (' ', '\t', '\n')
180 |
181 | def DEFN_BEGIN(self):
182 | self.match('<')
183 | self.match('-')
184 | return DEFN_BEGIN, '<-'
185 |
186 | def is_when(self):
187 | return self.ch == ':'
188 |
189 | def WHEN(self):
190 | self.match(':')
191 | self.match('-')
192 | return WHEN, ':-'
193 |
194 | def is_number(self):
195 | return self.ch in '0123456789'
196 |
197 | def is_num(self):
198 | return self.is_number() or self.ch in ('+', '-')
199 |
200 | def NUM(self):
201 | # get the leading sign
202 | sign = 1
203 | if self.ch == '+':
204 | self.eat()
205 | elif self.ch == '-':
206 | sign = -1
207 | self.eat()
208 |
209 | # read the whole part
210 | num = ''
211 | self.expect(self.is_number)
212 | while self.is_number():
213 | num += self.eat()
214 |
215 | if not self.ch == '.':
216 | return NUM, int(num)
217 | num += self.eat()
218 |
219 | # read the fractional part
220 | self.expect(self.is_number)
221 | while self.is_number():
222 | num += self.eat()
223 | return NUM, float(num)
224 |
225 | def is_ident(self):
226 | letters = 'abcdefghijklmnopqrstuvwxyz'
227 | return self.ch in letters or self.ch in letters.upper()
228 |
229 | def IDENT(self):
230 | ident = ''
231 | self.expect(self.is_ident)
232 | while self.is_ident() or self.is_number():
233 | ident += self.eat()
234 | return IDENT, ident
235 |
236 | def comment(self):
237 | self.match('#')
238 | while self.ch != '\n':
239 | self.eat()
240 |
241 | def next(self):
242 | while self.pos < len(self.line):
243 | if self.is_ws():
244 | self.eat()
245 | continue
246 | if self.ch == '#':
247 | self.comment()
248 | continue
249 | if self.ch == '<':
250 | return self.DEFN_BEGIN()
251 | if self.ch == '?':
252 | self.eat()
253 | if self.ch == '-':
254 | self.eat()
255 | return QUERY_BEGIN, '?-'
256 | return QUESTION, '?'
257 | if self.is_ident():
258 | return self.IDENT()
259 | if self.is_num():
260 | return self.NUM()
261 | if self.is_when():
262 | return self.WHEN()
263 | if self.ch == '(':
264 | return LPAREN, self.eat()
265 | if self.ch == ')':
266 | return RPAREN, self.eat()
267 | if self.ch == ',':
268 | return COMMA, self.eat()
269 | raise TokenError('no token begins with %s' % self.ch)
270 | return EOF, EOF
271 |
272 |
273 | def tokens(line):
274 | lexer = Lexer(line)
275 | while True:
276 | tokt, tok = lexer.next()
277 | if tokt == EOF:
278 | return
279 | yield tokt, tok
280 |
281 |
282 | def parse(line):
283 | p = Parser(Lexer(line))
284 | return p.command()
285 |
286 |
287 | def print_db(db):
288 | print 'Database:'
289 | longest = -1
290 | for pred in db:
291 | if len(pred) > longest:
292 | longest = len(pred)
293 | for pred, items in db.items():
294 | if not isinstance(items, list):
295 | continue
296 | print '%s:' % pred
297 | for item in items:
298 | print '\t', item
299 |
300 |
301 | def read_db(db_file):
302 | db = {}
303 | for line in db_file:
304 | if line == '\n': continue
305 | q = parse(line)
306 | if q:
307 | logic.store(db, q)
308 | return db
309 |
310 |
311 | ## Running
312 |
313 | help='''This interpreter provides basic functionality only--the subset of Prolog known
314 | as "Pure Prolog". That is, only clauses are supported--no lists, user-defined
315 | procedures, or arithmetic evaluation.
316 |
317 | The REPL allows both rule/fact definition as well as goal proving. The syntax
318 | is as follows:
319 |
320 | Defining rules and facts:
321 |
322 | <- this(is, ?a) :- simple(?rule), for(?demonstration, only)
323 | <- coprime(14, 15)
324 |
325 | Proving goals:
326 |
327 | ?- coprime(?x, 9)
328 |
329 | For some example rule databases, see `paip/examples/prolog`. They can be loaded
330 | with the `--db` option.
331 | '''
332 |
333 | argparser = argparse.ArgumentParser(description='A Prolog implementation.',
334 | formatter_class=argparse.RawDescriptionHelpFormatter,
335 | epilog=help)
336 |
337 | argparser.add_argument('--logging',
338 | action='store_true',
339 | help='Enable logging',
340 | dest='log')
341 | argparser.add_argument('--db',
342 | type=file,
343 | help='Database file',
344 | dest='db_file')
345 |
346 |
347 | def main():
348 | print 'Welcome to PyLogic. Type "help" for help.'
349 |
350 | args = argparser.parse_args()
351 | db = read_db(args.db_file) if args.db_file else {}
352 | if args.log:
353 | logging.basicConfig(level=logging.DEBUG)
354 |
355 | print_db(db)
356 | while True:
357 | try:
358 | line = raw_input('>> ')
359 | except EOFError:
360 | break
361 | if not line:
362 | continue
363 | if line == 'quit':
364 | break
365 | if line == 'help':
366 | print help
367 | continue
368 | try:
369 | q = parse(line)
370 | except ParseError as e:
371 | print e
372 | continue
373 | except TokenError as e:
374 | print e
375 | continue
376 |
377 | if isinstance(q, logic.Relation):
378 | try:
379 | logic.prolog_prove([q], db)
380 | except KeyboardInterrupt:
381 | print 'Cancelled.'
382 | elif isinstance(q, logic.Clause):
383 | logic.store(db, q)
384 | print_db(db)
385 |
386 | print 'Goodbye.'
387 |
388 |
389 | if __name__ == '__main__':
390 | main()
391 |
--------------------------------------------------------------------------------
/paip/search.py:
--------------------------------------------------------------------------------
1 | """
2 | Searching is one of the most useful strategies in AI programming. AI problems
3 | can often be expressed as state spaces with transitions between states. For
4 | instance, the General Problem Solver could be considered as a search
5 | problem--given some states, we apply state transitions to explore the state
6 | space until the goal is reached.
7 |
8 | For some example applications, see the following programs:
9 |
10 | - [Pathfinding](examples/search/pathfinding.html)
11 | - [GPS by searching](examples/search/gps.html)
12 |
13 | """
14 |
15 | # -----------------------------------------------------------------------------
16 | ## Tree Searches
17 |
18 | # Many problems find convenient expression as search trees of state spaces: each
19 | # state has some successor states, and we explore the "tree" of states formed by
20 | # linking each state to its successors. We can explore this state tree by
21 | # holding a set of "candidate", or "current", states, and exploring all its
22 | # successors until the goal is reached.
23 |
24 | ### The general case
25 |
26 | def tree_search(states, goal_reached, get_successors, combine_states):
27 | """
28 | Given some initial states, explore a state space until reaching the goal.
29 |
30 | `states` should be a list of initial states (which can be anything).
31 | `goal_reached` should be a predicate, where `goal_reached(state)` returns
32 | `True` when `state` is the goal state.
33 | `get_successors` should take a state as input and return a list of that
34 | state's successor states.
35 | `combine_states` should take two lists of states as input--the current
36 | states and a list of new states--and return a combined list of states.
37 |
38 | When the goal is reached, the goal state is returned.
39 | """
40 |
41 | # If there are no more states to explore, we have failed.
42 | if not states:
43 | return None
44 |
45 | if goal_reached(states[0]):
46 | return states[0]
47 |
48 | # Get the states that follow the first current state and combine them
49 | # with the other current states.
50 | successors = get_successors(states[0])
51 | next = combine_states(successors, states[1:])
52 |
53 | # Recursively search from the new list of current states.
54 | return tree_search(next, goal_reached, get_successors, combine_states)
55 |
56 |
57 | ### Depth-first search
58 |
59 | def dfs(start, goal_reached, get_successors):
60 | """
61 | A tree search where the state space is explored depth-first.
62 |
63 | That is, all of the successors of a single state are fully explored before
64 | exploring a sibling state.
65 | """
66 | def combine(new_states, existing_states):
67 | # The new states (successors of the first current state) should be
68 | # explored next, before the other states, so they are prepended to the
69 | # list of current states.
70 | return new_states + existing_states
71 | return tree_search([start], goal_reached, get_successors, combine)
72 |
73 |
74 | ### Breadth-first search
75 |
76 | def bfs(start, goal_reached, get_successors):
77 | """
78 | A tree search where the state space is explored breadth-first.
79 |
80 | That is, after examining a single state, all of its successors should be
81 | examined before any of their successors are explored.
82 | """
83 | def combine(new_states, existing_states):
84 | # Finish examining all of the sibling states before exploring any of
85 | # their successors--add all the new states at the end of the current
86 | # state list.
87 | return existing_states + new_states
88 | return tree_search([start], goal_reached, get_successors, combine)
89 |
90 |
91 | ### Best-first search
92 |
93 | def best_first_search(start, goal_reached, get_successors, cost):
94 | """
95 | A tree search where the state space is explored in order of "cost".
96 |
97 | That is, given a list of current states, the "cheapest" state (according
98 | to the function `cost`, which takes a state as input and returns a numerical
99 | cost value) is the next one explored.
100 | """
101 | def combine(new_states, existing_states):
102 | # Keep the list of current states ordered by cost--the "cheapest" state
103 | # should always be at the front of the list.
104 | return sorted(new_states + existing_states, key=cost)
105 | return tree_search([start], goal_reached, get_successors, combine)
106 |
107 |
108 | ### Beam search
109 |
110 | def beam_search(start, goal_reached, get_successors, cost, beam_width):
111 | """
112 | A tree search where the state space is explored by considering only the next
113 | `beam_width` cheapest states at any step.
114 |
115 | The downside to this approach is that by eliminating candidate states, the
116 | goal state might never be found!
117 | """
118 | def combine(new_states, existing_states):
119 | # To combine new and current states, combine and sort them as in
120 | # `best_first_search`, but take only the first `beam_width` states.
121 | return sorted(new_states + existing_states, key=cost)[:beam_width]
122 | return tree_search([start], goal_reached, get_successors, combine)
123 |
124 |
125 | ### Iterative-widening search
126 |
127 | def widening_search(start, goal_reached, successors, cost, width=1, max=100):
128 | """
129 | A tree search that repeatedly applies `beam_search` with incrementally
130 | increasing beam widths until the goal state is found. This strategy is more
131 | likely to find the goal state than a plain `beam_search`, but at the cost of
132 | exploring the state space more than once.
133 |
134 | `width` and `max` are the starting and maximum beam widths, respectively.
135 | """
136 | if width > max: # only increment up to max
137 | return
138 | # `beam_search` with the starting width and quit if we've reached the goal.
139 | res = beam_search(start, goal_reached, successors, cost, width)
140 | if res:
141 | return res
142 | # Otherwise, `beam_search` again with a higher beam width.
143 | else:
144 | return widening_search(start, goal_reached, successors, cost, width + 1)
145 |
146 |
147 | # -----------------------------------------------------------------------------
148 | ## Graph searches
149 |
150 | # For some problem domains, the state space is not really a tree--certain states
151 | # could form "cycles", where a successor of a current state is a state that has
152 | # been previously examined.
153 | #
154 | # The tree search algorithms we've discussed ignore this possibility and treat
155 | # every encountered state as distinct. This could lead to extra work, though,
156 | # as we re-explore certain branches. Graph search takes equivalent states into
157 | # account, keeps track of previously discarded states, and only explores states
158 | # that haven't already been encountered.
159 |
160 | ### The general case
161 |
162 | def graph_search(states, goal_reached, get_successors, combine, old_states=None):
163 | """
164 | Given some initial states, explore a state space until reaching the goal,
165 | taking care not to re-explore previously visited states.
166 |
167 | `states`, `goal_reached`, `get_successors`, and `combine` are identical to
168 | those arguments in `tree_search`.
169 | `old_states` is a list of previously encountered states--these should not
170 | be re-vistited during the search.
171 |
172 | When the goal is reached, the goal state is returned.
173 | """
174 | old_states = old_states or [] # initialize, if this is the initial call
175 |
176 | # Check for success and failure.
177 | if not states:
178 | return None
179 | if goal_reached(states[0]):
180 | return states[0]
181 |
182 | def visited(state):
183 | # A state is "visited" if it's in the list of current states or has
184 | # been encountered previously.
185 | return any(state == s for s in states + old_states)
186 |
187 | # Filter out the "visited" states from the next state's successors.
188 | new_states = [s for s in get_successors(states[0]) if not visited(s)]
189 |
190 | # Combine the new states with the existing ones and recurse.
191 | next_states = combine(new_states, states[1:])
192 | return graph_search(next_states, goal_reached, get_successors,
193 | combine, old_states + [states[0]])
194 |
195 | ### Exploration strategies
196 |
197 | # Just as for tree search, we can define special cases of graph search that use
198 | # specific exploration strategies: *breadth-first search* and *depth-first
199 | # search* are nearly identical as their tree-search varieties.
200 |
201 | def graph_search_bfs(start, goal_reached, get_successors, old_states=None):
202 | def combine(new_states, existing_states):
203 | return existing_states + new_states
204 | return graph_search([start], goal_reached, get_successors, combine,
205 | old_states)
206 |
207 |
208 | def graph_search_dfs(start, goal_reached, get_successors, old_states=None):
209 | def combine(new_states, existing_states):
210 | return new_states + existing_states
211 | return graph_search([start], goal_reached, get_successors, combine,
212 | old_states)
213 |
214 |
215 | # -----------------------------------------------------------------------------
216 | ## Application: Pathfinding
217 |
218 | # A common use of searching is in finding the best path between two locations.
219 | # This might be useful for planning airline routes or video game character
220 | # movements. We will develop a specialized pathfinding algorithm that uses
221 | # graph search on path segments to find the shortest path between two points.
222 |
223 | ### Path utilities
224 |
225 | # We first develop some utilities for handling paths and path segments.
226 |
227 | class Path(object):
228 | """`Path` represents one segment of a path traversing a state space."""
229 | def __init__(self, state, prev_path=None, cost=0):
230 | """
231 | Create a new path segment by linking `state` to the branch indicated
232 | by `prev_path`, where the cost of the path up to (and including) `state`
233 | is `cost`.
234 | """
235 | self.state = state
236 | self.prev_path = prev_path
237 | self.cost = cost
238 |
239 | def __repr__(self):
240 | return 'Path(%s, %s, %s)' % (self.state, self.prev_path, self.cost)
241 |
242 | def collect(self):
243 | states = [self.state]
244 | if self.prev_path:
245 | states = self.prev_path.collect() + states
246 | return states
247 |
248 |
249 | def find_path(to_state, paths):
250 | for path in paths:
251 | if to_state == path.state:
252 | return path
253 |
254 |
255 | def insert_path(path, paths, compare):
256 | """
257 | When inserting a path into an existing list of paths, we want to keep the
258 | list sorted with respect to some comparison function `compare`, which
259 | takes two `Path`s as arguments and returns a number.
260 | """
261 | for i in xrange(len(paths)):
262 | if compare(path, paths[i]) <= 0:
263 | paths.insert(i, path)
264 | return
265 | paths.append(path)
266 |
267 |
268 | def replace_if_better(path, compare, look_in, replace_in):
269 | """
270 | Search the list `look_in` for a path that ends at the same state as `path`.
271 | If found, remove that existing path and insert `path` into the list
272 | `replace_in`. Returns True if replacement occurred and False otherwise.
273 | """
274 | existing = find_path(path.state, look_in)
275 | if existing and compare(path, existing) < 0:
276 | look_in.remove(existing)
277 | insert_path(path, replace_in, compare)
278 | return True
279 | return False
280 |
281 | def extend_path(path, to_states, current_paths, old_paths, cost, compare):
282 | """
283 | To grow the list of `current_paths` to include the states in `to_states`,
284 | we will extend `path` to each state (assuming the new paths are shorter than
285 | the ones that already exist).
286 | """
287 | for state in to_states:
288 | # Extend `path` to each new state, updating the cost by adding the
289 | # cost of this extension to the existing path cost.
290 | extend_cost = path.cost + cost(path.state, state)
291 | extended = Path(state, path, extend_cost)
292 |
293 | # Replace any path in `current_paths` or `old_paths` that ends at
294 | # `state` if our new extended path is cheaper.
295 | if find_path(state, current_paths):
296 | replace_if_better(extended, compare, current_paths, current_paths)
297 | elif find_path(state, old_paths):
298 | replace_if_better(extended, compare, old_paths, current_paths)
299 | else:
300 | # If no existing path goes to `path`, just add our new one to the
301 | # end of `current_paths`.
302 | insert_path(extended, current_paths, compare)
303 |
304 |
305 | ### A* Search
306 |
307 | # A\* is a graph search that finds the shortest-path distance from a start state
308 | # to an end state. It works by incrementally extending paths from the start
309 | # state in order of cost and replacing previous paths when shorter ones are
310 | # found that reach the same state.
311 |
312 | # A heuristic function can be supplied to add additional cost to the cost of
313 | # each path; for standard A* search, this function measures the estimated
314 | # distance remaining from the end of a path to the desired goal state.
315 | # Supplying the zero function turns this into the well-known Dijkstra's
316 | # algorithm.
317 |
318 | def a_star(paths, goal_reached, get_successors, cost, heuristic, old_paths=None):
319 | """
320 | Find the shortest path that satisfies `goal_reached`. The function
321 | `heuristic` can be used to specify an ordering strategy among equal-length
322 | paths.
323 | """
324 | old_paths = old_paths or []
325 |
326 | # First check to see if we're done.
327 | if not paths:
328 | return None
329 | if goal_reached(paths[0].state):
330 | return paths[0]
331 |
332 | # We will keep the lists of currently-exploring and previously-explored
333 | # paths ordered by cost, where the cost of a path is computed as the sum
334 | # of the costs of the path segments and the heuristic applied to the final
335 | # state in the path.
336 | def compare(path1, path2):
337 | return ((path1.cost + heuristic(path1.state)) -
338 | (path2.cost + heuristic(path2.state)))
339 |
340 | # At each step, we extend the shortest path we've encountered so far.
341 | path = paths.pop(0)
342 |
343 | # We keep track of all previously seen paths in `old_paths`, so that we can
344 | # weed out newly-extended paths that are no better than previously discovered
345 | # paths to the same state.
346 | insert_path(path, old_paths, compare)
347 |
348 | # Extend our shortest path to all its possible successor states using
349 | # `extend_path`, which will make sure that `paths` and `old_paths` stay
350 | # sorted appropriately and weed out redundant paths.
351 | extend_path(path, get_successors(path.state), paths, old_paths, cost, compare)
352 |
353 | # Repeat with the newly-extended paths.
354 | return a_star(paths, goal_reached, get_successors, cost, heuristic, old_paths)
355 |
--------------------------------------------------------------------------------
/paip/tests/test_logic.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import unittest
3 | from paip import logic
4 |
5 |
6 | class VarTests(unittest.TestCase):
7 | def test_lookup_none(self):
8 | bindings = {}
9 | var = logic.Var('x')
10 | self.assertEqual(None, var.lookup(bindings))
11 |
12 | def test_lookup_immediate(self):
13 | x = logic.Var('x')
14 | y = logic.Atom('y')
15 | bindings = {x: y}
16 | self.assertEqual(y, x.lookup(bindings))
17 |
18 | def test_lookup_search(self):
19 | x = logic.Var('x')
20 | y = logic.Var('y')
21 | z = logic.Var('z')
22 | w = logic.Atom('w')
23 | bindings = {
24 | x: y,
25 | y: z,
26 | z: w
27 | }
28 | self.assertEqual(w, x.lookup(bindings))
29 |
30 | def test_lookup_search_no_atom(self):
31 | x = logic.Var('x')
32 | y = logic.Var('y')
33 | z = logic.Var('z')
34 | bindings = {
35 | x: y,
36 | y: z,
37 | }
38 | self.assertEqual(z, x.lookup(bindings))
39 |
40 | def test_rename_vars(self):
41 | v1 = logic.Var('x')
42 | begin = logic.Var.counter
43 | v2 = v1.rename_vars({v1: logic.Var.get_unused_var()})
44 |
45 | self.assertEqual(logic.Var('var%d' % begin), v2)
46 | self.assertEqual(begin + 1, logic.Var.counter)
47 |
48 | def test_get_vars(self):
49 | x = logic.Var('x')
50 | self.assertEqual([x], x.get_vars())
51 |
52 |
53 | class RelationTests(unittest.TestCase):
54 | def test_bind_vars(self):
55 | a = logic.Atom('a')
56 | b = logic.Atom('b')
57 | x = logic.Var('x')
58 | y = logic.Var('y')
59 |
60 | r = logic.Relation('likes', (a, x, y))
61 | s = logic.Relation('likes', (a, b, y))
62 | bindings = { x: b }
63 | self.assertEqual(s, r.bind_vars(bindings))
64 |
65 | def test_rename_vars(self):
66 | a = logic.Atom('a')
67 | x = logic.Var('x')
68 | y = logic.Var('y')
69 | p2 = logic.Relation('pair', (a, y))
70 | p1 = logic.Relation('pair', (x, p2))
71 | vs = p1.get_vars()
72 |
73 | begin = logic.Var.counter
74 | rep = {v: logic.Var.get_unused_var() for v in vs}
75 |
76 | u = logic.Var('var%d' % begin)
77 | v = logic.Var('var%d' % (begin+1))
78 | r = logic.Relation('pair', [u, logic.Relation('pair', [a, v])])
79 | self.assertEqual(r, p1.rename_vars(rep))
80 |
81 | def test_rename_repeated_var(self):
82 | x = logic.Var('x')
83 | y = logic.Var('y')
84 | r = logic.Relation('likes', (x, x))
85 | s = logic.Relation('likes', (y, y))
86 | self.assertEqual(s, r.rename_vars({x: y}))
87 |
88 | def test_get_vars(self):
89 | a = logic.Atom('a')
90 | x = logic.Var('x')
91 | y = logic.Var('y')
92 | p3 = logic.Relation('pair', (x, x))
93 | p2 = logic.Relation('pair', (a, p3))
94 | p1 = logic.Relation('pair', (y, p2))
95 | self.assertEqual(set([x, y]), set(p1.get_vars()))
96 |
97 |
98 | class ClauseTests(unittest.TestCase):
99 | def test_bind_vars(self):
100 | a = logic.Atom('a')
101 | b = logic.Atom('b')
102 | c = logic.Atom('c')
103 | x = logic.Var('x')
104 | y = logic.Var('y')
105 | z = logic.Var('z')
106 |
107 | r = logic.Relation('likes', (x, y, a))
108 | s = logic.Relation('likes', (y, a, z))
109 | t = logic.Relation('hates', (z, b, x))
110 |
111 | bindings = { z: c, y: b, x: y }
112 | cl1 = logic.Clause(r, (s, t))
113 | cl2 = logic.Clause(
114 | logic.Relation('likes', (b, b, a)),
115 | (logic.Relation('likes', (b, a, c)),
116 | logic.Relation('hates', (c, b, b))))
117 |
118 | self.assertEqual(cl2, cl1.bind_vars(bindings))
119 |
120 | def test_rename_vars(self):
121 | x = logic.Var('x')
122 | y = logic.Var('y')
123 | z = logic.Var('z')
124 | p = logic.Relation('pair', (y, logic.Relation('pair', (x, z))))
125 | is_member = logic.Relation('member', (x, p))
126 | is_list = logic.Relation('is_list', [p])
127 | rule = logic.Clause(is_member, (is_list, p))
128 |
129 | vs = rule.get_vars()
130 | begin = logic.Var.counter
131 | renames = {v: logic.Var.get_unused_var() for v in vs}
132 | rule2 = rule.rename_vars(renames)
133 |
134 | newx = renames[x]
135 | newy = renames[y]
136 | newz = renames[z]
137 | new_list = logic.Relation('pair',
138 | (newy, logic.Relation('pair', (newx, newz))))
139 | rule3 = logic.Clause(logic.Relation('member', (newx, new_list)),
140 | (logic.Relation('is_list', [new_list]), new_list))
141 |
142 | self.assertEqual(rule3, rule2)
143 |
144 | def test_recursive_rename(self):
145 | list = logic.Var('list')
146 | x = logic.Var('x')
147 | y = logic.Var('y')
148 | z = logic.Var('z')
149 |
150 | member = logic.Clause(logic.Relation('member', (x, list)),
151 | [logic.Relation('first', (list, y)),
152 | logic.Relation('rest', (list, z)),
153 | logic.Relation('member', (x, z))])
154 |
155 | renamed = member.recursive_rename()
156 | bindings = logic.unify(renamed, member, {})
157 |
158 | self.assertTrue(x in bindings or x in bindings.values())
159 | self.assertTrue(y in bindings or y in bindings.values())
160 | self.assertTrue(z in bindings or z in bindings.values())
161 |
162 | def test_get_vars(self):
163 | a = logic.Atom('a')
164 | b = logic.Atom('b')
165 | x = logic.Var('x')
166 | y = logic.Var('y')
167 | z = logic.Var('z')
168 | r = logic.Relation('likes', (a, x))
169 | s = logic.Relation('likes', (y, b))
170 | t = logic.Relation('hates', (x, z))
171 | c = logic.Clause(r, (s, t))
172 | self.assertEqual(set([x, y, z]), set(c.get_vars()))
173 |
174 |
175 | class UnificationTests(unittest.TestCase):
176 | def test_atom_atom_ok(self):
177 | a = logic.Atom('a')
178 | self.assertEqual({}, logic.unify(a, a, {}))
179 |
180 | def test_atom_atom_fail(self):
181 | a = logic.Atom('a')
182 | b = logic.Atom('b')
183 | self.assertFalse(logic.unify(a, b, {}))
184 |
185 | def test_atom_var_exists_ok(self):
186 | a = logic.Atom('a')
187 | x = logic.Var('x')
188 | bindings = {x: a}
189 | self.assertEqual(bindings, logic.unify(a, x, bindings))
190 |
191 | def test_atom_var_exists_fail(self):
192 | a = logic.Atom('a')
193 | b = logic.Atom('b')
194 | x = logic.Var('x')
195 | bindings = {x: b}
196 | self.assertFalse(logic.unify(a, x, bindings))
197 |
198 | def test_atom_var_new(self):
199 | a = logic.Atom('a')
200 | x = logic.Var('x')
201 | self.assertEqual({x: a}, logic.unify(a, x, {}))
202 |
203 | def test_var_var_both_unbound(self):
204 | x = logic.Var('x')
205 | y = logic.Var('y')
206 | self.assertEqual({x: y}, logic.unify(x, y, {}))
207 |
208 | def test_var_var_left_unbound(self):
209 | x = logic.Var('x')
210 | y = logic.Var('y')
211 | a = logic.Atom('a')
212 | bindings = {x: a}
213 | self.assertEqual({x: a, y: a}, logic.unify(y, x, bindings))
214 |
215 | def test_var_var_right_unbound(self):
216 | x = logic.Var('x')
217 | y = logic.Var('y')
218 | a = logic.Atom('a')
219 | bindings = {x: a}
220 | self.assertEqual({x: a, y: a}, logic.unify(x, y, bindings))
221 |
222 | def test_var_var_both_bound_equal(self):
223 | x = logic.Var('x')
224 | y = logic.Var('y')
225 | a = logic.Atom('a')
226 | bindings = {x: a, y: a}
227 | self.assertEqual(bindings, logic.unify(x, y, bindings))
228 |
229 | def test_var_var_both_bound_unequal(self):
230 | x = logic.Var('x')
231 | y = logic.Var('y')
232 | a = logic.Atom('a')
233 | b = logic.Atom('b')
234 | bindings = {x: a, y: b}
235 | self.assertFalse(logic.unify(x, y, bindings))
236 |
237 | def test_var_relation(self):
238 | x = logic.Var('x')
239 | r = logic.Relation('foo', (logic.Var('bar'), logic.Atom('baz')))
240 | bindings = {x: r}
241 | self.assertEqual(bindings, logic.unify(x, r, {}))
242 |
243 | def test_var_var_resolves_to_relation(self):
244 | x = logic.Var('x')
245 | y = logic.Var('y')
246 | r = logic.Relation('foo', (logic.Var('bar'), logic.Atom('baz')))
247 | bindings = {x: r, y: r}
248 | self.assertEqual(bindings, logic.unify(x, y, {y: r}))
249 |
250 | def test_var_resolves_to_relation_var(self):
251 | x = logic.Var('x')
252 | y = logic.Var('y')
253 | r = logic.Relation('foo', (logic.Var('bar'), logic.Atom('baz')))
254 | bindings = {y: r, x: r}
255 | self.assertEqual(bindings, logic.unify(x, y, {x: r}))
256 |
257 | def test_var_var_both_resolve_to_relations(self):
258 | x = logic.Var('x')
259 | y = logic.Var('y')
260 | bar = logic.Var('bar')
261 | baz = logic.Atom('baz')
262 | b = logic.Atom('b')
263 | c = logic.Var('c')
264 | r = logic.Relation('foo', (bar, baz))
265 | s = logic.Relation('foo', (b, c))
266 | bindings = {x: r, y: s, bar: b, c: baz}
267 | self.assertEqual(bindings, logic.unify(x, y, {x: r, y: s}))
268 |
269 | def test_relation_relation_different_preds(self):
270 | x = logic.Var('x')
271 | y = logic.Var('y')
272 | a = logic.Atom('a')
273 | r = logic.Relation('likes', (x, y))
274 | s = logic.Relation('loves', (x, a))
275 | self.assertFalse(logic.unify(r, s, {}))
276 |
277 | def test_relation_relation_different_lengths(self):
278 | x = logic.Var('x')
279 | y = logic.Var('y')
280 | a = logic.Atom('a')
281 | r = logic.Relation('likes', (x, y))
282 | s = logic.Relation('likes', (x, a, y))
283 | self.assertFalse(logic.unify(r, s, {}))
284 |
285 | def test_relation_relation_different_args(self):
286 | x = logic.Var('x')
287 | y = logic.Var('y')
288 | a = logic.Atom('a')
289 | b = logic.Atom('b')
290 | r = logic.Relation('likes', (x, a))
291 | s = logic.Relation('likes', (y, b))
292 | self.assertFalse(logic.unify(r, s, {}))
293 |
294 | def test_relation_relation_ok(self):
295 | x = logic.Var('x')
296 | y = logic.Var('y')
297 | a = logic.Atom('a')
298 | b = logic.Atom('b')
299 | r = logic.Relation('likes', (x, y))
300 | s = logic.Relation('likes', (a, x))
301 | self.assertEqual({x: a, y: a}, logic.unify(r, s, {}))
302 |
303 | def test_clauses_different_heads(self):
304 | joe = logic.Atom('joe')
305 | judy = logic.Atom('judy')
306 | jorge = logic.Atom('jorge')
307 | x = logic.Var('x')
308 | r = logic.Relation('likes', (joe, x))
309 | s = logic.Relation('likes', (joe, judy))
310 | t = logic.Relation('hates', (x, jorge))
311 | c = logic.Clause(r, [s])
312 | d = logic.Clause(t, [s])
313 | self.assertFalse(logic.unify(c, d, {}))
314 |
315 | def test_clauses_different_length_bodies(self):
316 | joe = logic.Atom('joe')
317 | judy = logic.Atom('judy')
318 | jorge = logic.Atom('jorge')
319 | x = logic.Var('x')
320 | y = logic.Var('y')
321 | r = logic.Relation('likes', (joe, x))
322 | s = logic.Relation('hates', (joe, judy))
323 | t = logic.Relation('likes', (y, jorge))
324 | u = logic.Relation('hates', (joe, jorge))
325 | c = logic.Clause(r, [s])
326 | d = logic.Clause(t, [s, u])
327 | self.assertFalse(logic.unify(c, d, {}))
328 |
329 | def test_clauses_different_bodies(self):
330 | joe = logic.Atom('joe')
331 | judy = logic.Atom('judy')
332 | jorge = logic.Atom('jorge')
333 | x = logic.Var('x')
334 | y = logic.Var('y')
335 | r = logic.Relation('likes', (joe, x))
336 | s = logic.Relation('hates', (joe, judy))
337 | t = logic.Relation('likes', (y, jorge))
338 | u = logic.Relation('hates', (judy, joe))
339 | v = logic.Relation('hates', (judy, x))
340 | c = logic.Clause(r, [s, v])
341 | d = logic.Clause(t, [s, u])
342 | self.assertFalse(logic.unify(c, d, {}))
343 |
344 | def test_clauses_ok(self):
345 | joe = logic.Atom('joe')
346 | judy = logic.Atom('judy')
347 | jorge = logic.Atom('jorge')
348 | x = logic.Var('x')
349 | y = logic.Var('y')
350 | r = logic.Relation('likes', (joe, x))
351 | s = logic.Relation('hates', (joe, judy))
352 | t = logic.Relation('likes', (y, jorge))
353 | u = logic.Relation('hates', (judy, joe))
354 | v = logic.Relation('hates', (judy, y))
355 | c = logic.Clause(r, [s, v])
356 | d = logic.Clause(t, [s, u])
357 | self.assertEqual({x: jorge, y: joe}, logic.unify(c, d, {}))
358 |
359 |
360 | class ProveTests(unittest.TestCase):
361 | def test_prove_no_relevant_clauses(self):
362 | joe = logic.Atom('joe')
363 | judy = logic.Atom('judy')
364 | jorge = logic.Atom('jorge')
365 | x = logic.Var('x')
366 |
367 | db = {'likes': []}
368 | db['likes'].append(logic.Clause(logic.Relation('likes', (joe, x)),
369 | [logic.Relation('likes', (x, joe)),
370 | logic.Relation('hates', (judy, x))]))
371 | db['likes'].append(logic.Clause(logic.Relation('likes', (jorge, judy))))
372 |
373 | goal = logic.Relation('hates', (joe, x))
374 | bindings = logic.prove(goal, {}, db)
375 | self.assertFalse(bindings)
376 |
377 | def test_prove_no_subgoals_required(self):
378 | joe = logic.Atom('joe')
379 | judy = logic.Atom('judy')
380 | jorge = logic.Atom('jorge')
381 | x = logic.Var('x')
382 |
383 | db = {'likes': []}
384 | db['likes'].append(logic.Clause(logic.Relation('likes', (joe, x)),
385 | [logic.Relation('likes', (x, joe)),
386 | logic.Relation('hates', (judy, x))]))
387 | db['likes'].append(logic.Clause(logic.Relation('likes', (jorge, judy))))
388 |
389 | goal = logic.Relation('likes', (jorge, x))
390 | bindings = logic.prove(goal, {}, db)
391 | self.assertEqual({x: judy}, bindings)
392 |
393 | def test_prove_all_no_subgoals_required(self):
394 | joe = logic.Atom('joe')
395 | judy = logic.Atom('judy')
396 | jorge = logic.Atom('jorge')
397 | x = logic.Var('x')
398 |
399 | db = {'likes': [], 'hates': []}
400 | db['likes'].append(logic.Clause(logic.Relation('likes', (joe, x)),
401 | [logic.Relation('likes', (x, joe)),
402 | logic.Relation('hates', (judy, x))]))
403 | db['likes'].append(logic.Clause(logic.Relation('likes', (jorge, joe))))
404 | db['hates'].append(logic.Clause(logic.Relation('hates', (judy, jorge))))
405 |
406 | goal1 = logic.Relation('likes', (x, joe))
407 | goal2 = logic.Relation('hates', (judy, x))
408 | bindings = logic.prove_all([goal1, goal2], {}, db)
409 | self.assertEqual({x: jorge}, bindings)
410 |
411 | def test_prove_subgoals_required_fail(self):
412 | joe = logic.Atom('joe')
413 | judy = logic.Atom('judy')
414 | jorge = logic.Atom('jorge')
415 | x = logic.Var('x')
416 |
417 | db = {'likes': [], 'hates': []}
418 | db['likes'].append(logic.Clause(logic.Relation('likes', (joe, x)),
419 | [logic.Relation('likes', (x, joe)),
420 | logic.Relation('hates', (judy, x))]))
421 | db['likes'].append(logic.Clause(logic.Relation('likes', (jorge, joe))))
422 | db['hates'].append(logic.Clause(logic.Relation('hates', (judy, joe))))
423 |
424 | goal = logic.Relation('likes', (joe, jorge))
425 | bindings = logic.prove(goal, {}, db)
426 | self.assertFalse(bindings)
427 |
428 | def test_prove_subgoals_required_pass(self):
429 | joe = logic.Atom('joe')
430 | judy = logic.Atom('judy')
431 | jorge = logic.Atom('jorge')
432 | x = logic.Var('x')
433 |
434 | db = {'likes': [], 'hates': []}
435 | db['likes'].append(logic.Clause(logic.Relation('likes', (joe, x)),
436 | [logic.Relation('likes', (x, joe)),
437 | logic.Relation('hates', (judy, x))]))
438 | db['likes'].append(logic.Clause(logic.Relation('likes', (jorge, joe))))
439 | db['hates'].append(logic.Clause(logic.Relation('hates', (judy, jorge))))
440 |
441 | goal = logic.Relation('likes', (joe, x))
442 | bindings = logic.prove(goal, {}, db)
443 | self.assertEqual(jorge, x.lookup(bindings))
444 |
445 | def test_prove_primitive_call(self):
446 | joe = logic.Atom('joe')
447 | judy = logic.Atom('judy')
448 | jorge = logic.Atom('jorge')
449 | x = logic.Var('x')
450 |
451 | db = {'likes': [], 'hates': []}
452 | db['likes'].append(logic.Clause(logic.Relation('likes', (joe, x)),
453 | [logic.Relation('likes', (x, joe)),
454 | logic.Relation('hates', (judy, x))]))
455 | db['likes'].append(logic.Clause(logic.Relation('likes', (jorge, joe))))
456 | db['hates'].append(logic.Clause(logic.Relation('hates', (judy, jorge))))
457 |
458 | things = []
459 | def prim(a, b, c, d):
460 | things.append(a)
461 | db['prim'] = prim
462 |
463 | goal = logic.Relation('likes', (joe, x))
464 | display = logic.Relation('prim', 'foo')
465 |
466 | bindings = logic.prove_all([goal, display], {}, db)
467 | self.assertEqual(['foo'], things)
468 |
--------------------------------------------------------------------------------
/paip/othello.py:
--------------------------------------------------------------------------------
1 | """
2 | **Othello** is a turn-based two-player strategy board game. The players take
3 | turns placing pieces--one player white and the other player black--on an 8x8
4 | board in such a way that captures some of the opponent's pieces, with the goal
5 | of finishing the game with more pieces of their color on the board.
6 |
7 | Every move must capture one more more of the opponent's pieces. To capture,
8 | player A places a piece adjacent to one of player B's pieces so that there is a
9 | straight line (horizontal, vertical, or diagonal) of adjacent pieces that begins
10 | with one of player A's pieces, continues with one more more of player B's
11 | pieces, and ends with one of player A's pieces.
12 |
13 | For example, if Black places a piece on square (5, 1), he will capture all of
14 | Black's pieces between (5, 1) and (5, 6):
15 |
16 | 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
17 | 1 . . . . . . . . 1 . . . . . . . .
18 | 2 . . . . . . . . 2 . . . . . . . .
19 | 3 . . o @ . o . . 3 . . o @ . o . .
20 | 4 . . o o @ @ . . 4 . . o o @ @ . .
21 | 5 . o o o o @ . . 5 @ @ @ @ @ @ . .
22 | 6 . . . @ o . . . 6 . . . @ o . . .
23 | 7 . . . . . . . . 7 . . . . . . . .
24 | 8 . . . . . . . . 8 . . . . . . . .
25 |
26 | For more more information about the game (which is also known as Reversi)
27 | including detailed rules, see the entry on [Wikipedia][wiki]. Additionally,
28 | this implementation doesn't take into account some tournament-style Othello
29 | details, such as game time limits and a different indexing scheme.
30 |
31 | We will implement representations for the board and pieces and the mechanics of
32 | playing a game. We will then explore several game-playing strategies. There is
33 | a simple command-line program [provided](examples/othello/othello.html) for
34 | playing against the computer or comparing two strategies.
35 |
36 | Written by [Daniel Connelly](http://dhconnelly.com). This implementation follows
37 | chapter 18 of Peter Norvig's "Paradigms of Artificial Intelligence".
38 |
39 | [wiki]: http://en.wikipedia.org/wiki/Reversi
40 |
41 | """
42 |
43 | # -----------------------------------------------------------------------------
44 | ## Table of contents
45 |
46 | # 1. [Board representation](#board)
47 | # 2. [Playing the game](#playing)
48 | # 3. [Strategies](#strategies)
49 | # - [Random](#random)
50 | # - [Local maximization](#localmax)
51 | # - [Minimax search](#minimax)
52 | # - [Alpha-beta search](#alphabeta)
53 | # 4. [Conclusion](#conclusion)
54 |
55 |
56 | # -----------------------------------------------------------------------------
57 | #
58 | ## Board representation
59 |
60 | # We represent the board as a 100-element list, which includes each square on
61 | # the board as well as the outside edge. Each consecutive sublist of ten
62 | # elements represents a single row, and each list element stores a piece. An
63 | # initial board contains four pieces in the center:
64 |
65 | # ? ? ? ? ? ? ? ? ? ?
66 | # ? . . . . . . . . ?
67 | # ? . . . . . . . . ?
68 | # ? . . . . . . . . ?
69 | # ? . . . o @ . . . ?
70 | # ? . . . @ o . . . ?
71 | # ? . . . . . . . . ?
72 | # ? . . . . . . . . ?
73 | # ? . . . . . . . . ?
74 | # ? ? ? ? ? ? ? ? ? ?
75 |
76 | # This representation has two useful properties:
77 | #
78 | # 1. Square (m,n) can be accessed as `board[mn]`. This avoids the need to write
79 | # functions that convert between square locations and list indexes.
80 | # 2. Operations involving bounds checking are slightly simpler.
81 |
82 | # The outside edge is marked ?, empty squares are ., black is @, and white is o.
83 | # The black and white pieces represent the two players.
84 | EMPTY, BLACK, WHITE, OUTER = '.', '@', 'o', '?'
85 | PIECES = (EMPTY, BLACK, WHITE, OUTER)
86 | PLAYERS = {BLACK: 'Black', WHITE: 'White'}
87 |
88 | # To refer to neighbor squares we can add a direction to a square.
89 | UP, DOWN, LEFT, RIGHT = -10, 10, -1, 1
90 | UP_RIGHT, DOWN_RIGHT, DOWN_LEFT, UP_LEFT = -9, 11, 9, -11
91 | DIRECTIONS = (UP, UP_RIGHT, RIGHT, DOWN_RIGHT, DOWN, DOWN_LEFT, LEFT, UP_LEFT)
92 |
93 | def squares():
94 | """List all the valid squares on the board."""
95 | return [i for i in xrange(11, 89) if 1 <= (i % 10) <= 8]
96 |
97 | def initial_board():
98 | """Create a new board with the initial black and white positions filled."""
99 | board = [OUTER] * 100
100 | for i in squares():
101 | board[i] = EMPTY
102 | # The middle four squares should hold the initial piece positions.
103 | board[44], board[45] = WHITE, BLACK
104 | board[54], board[55] = BLACK, WHITE
105 | return board
106 |
107 | def print_board(board):
108 | """Get a string representation of the board."""
109 | rep = ''
110 | rep += ' %s\n' % ' '.join(map(str, range(1, 9)))
111 | for row in xrange(1, 9):
112 | begin, end = 10*row + 1, 10*row + 9
113 | rep += '%d %s\n' % (row, ' '.join(board[begin:end]))
114 | return rep
115 |
116 |
117 | # -----------------------------------------------------------------------------
118 | #
119 | ## Playing the game
120 |
121 | # We need functions to get moves from players, check to make sure that the moves
122 | # are legal, apply the moves to the board, and detect when the game is over.
123 |
124 | ### Checking moves
125 |
126 | # A move must be both valid and legal: it must refer to a real square, and it
127 | # must form a bracket with another piece of the same color with pieces of the
128 | # opposite color in between.
129 |
130 | def is_valid(move):
131 | """Is move a square on the board?"""
132 | return isinstance(move, int) and move in squares()
133 |
134 | def opponent(player):
135 | """Get player's opponent piece."""
136 | return BLACK if player is WHITE else WHITE
137 |
138 | def find_bracket(square, player, board, direction):
139 | """
140 | Find a square that forms a bracket with `square` for `player` in the given
141 | `direction`. Returns None if no such square exists.
142 | """
143 | bracket = square + direction
144 | if board[bracket] == player:
145 | return None
146 | opp = opponent(player)
147 | while board[bracket] == opp:
148 | bracket += direction
149 | return None if board[bracket] in (OUTER, EMPTY) else bracket
150 |
151 | def is_legal(move, player, board):
152 | """Is this a legal move for the player?"""
153 | hasbracket = lambda direction: find_bracket(move, player, board, direction)
154 | return board[move] == EMPTY and any(map(hasbracket, DIRECTIONS))
155 |
156 | ### Making moves
157 |
158 | # When the player makes a move, we need to update the board and flip all the
159 | # bracketed pieces.
160 |
161 | def make_move(move, player, board):
162 | """Update the board to reflect the move by the specified player."""
163 | board[move] = player
164 | for d in DIRECTIONS:
165 | make_flips(move, player, board, d)
166 | return board
167 |
168 | def make_flips(move, player, board, direction):
169 | """Flip pieces in the given direction as a result of the move by player."""
170 | bracket = find_bracket(move, player, board, direction)
171 | if not bracket:
172 | return
173 | square = move + direction
174 | while square != bracket:
175 | board[square] = player
176 | square += direction
177 |
178 | ### Monitoring players
179 |
180 | class IllegalMoveError(Exception):
181 | def __init__(self, player, move, board):
182 | self.player = player
183 | self.move = move
184 | self.board = board
185 |
186 | def __str__(self):
187 | return '%s cannot move to square %d' % (PLAYERS[self.player], self.move)
188 |
189 | def legal_moves(player, board):
190 | """Get a list of all legal moves for player."""
191 | return [sq for sq in squares() if is_legal(sq, player, board)]
192 |
193 | def any_legal_move(player, board):
194 | """Can player make any moves?"""
195 | return any(is_legal(sq, player, board) for sq in squares())
196 |
197 | ### Putting it all together
198 |
199 | # Each round consists of:
200 | #
201 | # - Get a move from the current player.
202 | # - Apply it to the board.
203 | # - Switch players. If the game is over, get the final score.
204 |
205 | def play(black_strategy, white_strategy):
206 | """Play a game of Othello and return the final board and score."""
207 | board = initial_board()
208 | player = BLACK
209 | strategy = lambda who: black_strategy if who == BLACK else white_strategy
210 | while player is not None:
211 | move = get_move(strategy(player), player, board)
212 | make_move(move, player, board)
213 | player = next_player(board, player)
214 | return board, score(BLACK, board)
215 |
216 | def next_player(board, prev_player):
217 | """Which player should move next? Returns None if no legal moves exist."""
218 | opp = opponent(prev_player)
219 | if any_legal_move(opp, board):
220 | return opp
221 | elif any_legal_move(prev_player, board):
222 | return prev_player
223 | return None
224 |
225 | def get_move(strategy, player, board):
226 | """Call strategy(player, board) to get a move."""
227 | copy = list(board) # copy the board to prevent cheating
228 | move = strategy(player, copy)
229 | if not is_valid(move) or not is_legal(move, player, board):
230 | raise IllegalMoveError(player, move, copy)
231 | return move
232 |
233 | def score(player, board):
234 | """Compute player's score (number of player's pieces minus opponent's)."""
235 | mine, theirs = 0, 0
236 | opp = opponent(player)
237 | for sq in squares():
238 | piece = board[sq]
239 | if piece == player: mine += 1
240 | elif piece == opp: theirs += 1
241 | return mine - theirs
242 |
243 |
244 | # -----------------------------------------------------------------------------
245 | #
246 | ## Play strategies
247 |
248 | #
249 | ### Random
250 |
251 | # The easiest strategy to implement simply picks a move at random.
252 |
253 | import random
254 |
255 | def random_strategy(player, board):
256 | """A strategy that always chooses a random legal move."""
257 | return random.choice(legal_moves(player, board))
258 |
259 | #
260 | ### Local maximization
261 |
262 | # A more sophisticated strategy could look at every available move and evaluate
263 | # them in some way. This consists of getting a list of legal moves, applying
264 | # each one to a copy of the board, and choosing the move that results in the
265 | # "best" board.
266 |
267 | def maximizer(evaluate):
268 | """
269 | Construct a strategy that chooses the best move by maximizing
270 | evaluate(player, board) over all boards resulting from legal moves.
271 | """
272 | def strategy(player, board):
273 | def score_move(move):
274 | return evaluate(player, make_move(move, player, list(board)))
275 | return max(legal_moves(player, board), key=score_move)
276 | return strategy
277 |
278 | # One possible evaluation function is `score`. A strategy constructed with
279 | # `maximizer(score)` will always make the move that results in the largest
280 | # immediate gain in pieces.
281 |
282 | # A more advanced evaluation function might consider the relative worth of each
283 | # square on the board and weight the score by the value of the pieces held by
284 | # each player. Since corners and (most) edge squares are very valuable, we
285 | # could weight those more heavily, and add negative weights to the squares that,
286 | # if acquired, could lead to the opponent capturing the corners or edges.
287 |
288 | SQUARE_WEIGHTS = [
289 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
290 | 0, 120, -20, 20, 5, 5, 20, -20, 120, 0,
291 | 0, -20, -40, -5, -5, -5, -5, -40, -20, 0,
292 | 0, 20, -5, 15, 3, 3, 15, -5, 20, 0,
293 | 0, 5, -5, 3, 3, 3, 3, -5, 5, 0,
294 | 0, 5, -5, 3, 3, 3, 3, -5, 5, 0,
295 | 0, 20, -5, 15, 3, 3, 15, -5, 20, 0,
296 | 0, -20, -40, -5, -5, -5, -5, -40, -20, 0,
297 | 0, 120, -20, 20, 5, 5, 20, -20, 120, 0,
298 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
299 | ]
300 |
301 | # A strategy constructed as `maximizer(weighted_score)`, then, will always
302 | # return the move that results in the largest immediate *weighted* gain in
303 | # pieces.
304 |
305 | def weighted_score(player, board):
306 | """
307 | Compute the difference between the sum of the weights of player's
308 | squares and the sum of the weights of opponent's squares.
309 | """
310 | opp = opponent(player)
311 | total = 0
312 | for sq in squares():
313 | if board[sq] == player:
314 | total += SQUARE_WEIGHTS[sq]
315 | elif board[sq] == opp:
316 | total -= SQUARE_WEIGHTS[sq]
317 | return total
318 |
319 | #
320 | ### Minimax search
321 |
322 | # The maximizer strategies are very short-sighted, and a player who can consider
323 | # the implications of a move several turns in advance could have a significant
324 | # advantage. The **minimax** algorithm does just that.
325 |
326 | def minimax(player, board, depth, evaluate):
327 | """
328 | Find the best legal move for player, searching to the specified depth.
329 | Returns a tuple (move, min_score), where min_score is the guaranteed minimum
330 | score achievable for player if the move is made.
331 | """
332 |
333 | # We define the value of a board to be the opposite of its value to our
334 | # opponent, computed by recursively applying `minimax` for our opponent.
335 | def value(board):
336 | return -minimax(opponent(player), board, depth-1, evaluate)[0]
337 |
338 | # When depth is zero, don't examine possible moves--just determine the value
339 | # of this board to the player.
340 | if depth == 0:
341 | return evaluate(player, board), None
342 |
343 | # We want to evaluate all the legal moves by considering their implications
344 | # `depth` turns in advance. First, find all the legal moves.
345 | moves = legal_moves(player, board)
346 |
347 | # If player has no legal moves, then either:
348 | if not moves:
349 | # the game is over, so the best achievable score is victory or defeat;
350 | if not any_legal_move(opponent(player), board):
351 | return final_value(player, board), None
352 | # or we have to pass this turn, so just find the value of this board.
353 | return value(board), None
354 |
355 | # When there are multiple legal moves available, choose the best one by
356 | # maximizing the value of the resulting boards.
357 | return max((value(make_move(m, player, list(board))), m) for m in moves)
358 |
359 | # Values for endgame boards are big constants.
360 | MAX_VALUE = sum(map(abs, SQUARE_WEIGHTS))
361 | MIN_VALUE = -MAX_VALUE
362 |
363 | def final_value(player, board):
364 | """The game is over--find the value of this board to player."""
365 | diff = score(player, board)
366 | if diff < 0:
367 | return MIN_VALUE
368 | elif diff > 0:
369 | return MAX_VALUE
370 | return diff
371 |
372 | def minimax_searcher(depth, evaluate):
373 | """
374 | Construct a strategy that uses `minimax` with the specified leaf board
375 | evaluation function.
376 | """
377 | def strategy(player, board):
378 | return minimax(player, board, depth, evaluate)[1]
379 | return strategy
380 |
381 | #
382 | ### Alpha-Beta search
383 |
384 | # Minimax is very effective, but it does too much work: it evaluates many search
385 | # trees that should be ignored.
386 |
387 | # Consider what happens when minimax is evaluating two moves, M1 and M2, on one
388 | # level of a search tree. Suppose minimax determines that M1 can result in a
389 | # score of S. While evaluating M2, if minimax finds a move in its subtree that
390 | # could result in a better score than S, the algorithm should immediately quit
391 | # evaluating M2: the opponent will force us to play M1 to avoid the higher score
392 | # resulting from M1, so we shouldn't waste time determining just how much better
393 | # M2 is than M1.
394 |
395 | # We need to keep track of two values:
396 | #
397 | # - alpha: the maximum score achievable by any of the moves we have encountered.
398 | # - beta: the score that the opponent can keep us under by playing other moves.
399 | #
400 | # When the algorithm begins, alpha is the smallest value and beta is the largest
401 | # value. During evaluation, if we find a move that causes `alpha >= beta`, then
402 | # we can quit searching this subtree since the opponent can prevent us from
403 | # playing it.
404 |
405 | def alphabeta(player, board, alpha, beta, depth, evaluate):
406 | """
407 | Find the best legal move for player, searching to the specified depth. Like
408 | minimax, but uses the bounds alpha and beta to prune branches.
409 | """
410 | if depth == 0:
411 | return evaluate(player, board), None
412 |
413 | def value(board, alpha, beta):
414 | # Like in `minimax`, the value of a board is the opposite of its value
415 | # to the opponent. We pass in `-beta` and `-alpha` as the alpha and
416 | # beta values, respectively, for the opponent, since `alpha` represents
417 | # the best score we know we can achieve and is therefore the worst score
418 | # achievable by the opponent. Similarly, `beta` is the worst score that
419 | # our opponent can hold us to, so it is the best score that they can
420 | # achieve.
421 | return -alphabeta(opponent(player), board, -beta, -alpha, depth-1, evaluate)[0]
422 |
423 | moves = legal_moves(player, board)
424 | if not moves:
425 | if not any_legal_move(opponent(player), board):
426 | return final_value(player, board), None
427 | return value(board, alpha, beta), None
428 |
429 | best_move = moves[0]
430 | for move in moves:
431 | if alpha >= beta:
432 | # If one of the legal moves leads to a better score than beta, then
433 | # the opponent will avoid this branch, so we can quit looking.
434 | break
435 | val = value(make_move(move, player, list(board)), alpha, beta)
436 | if val > alpha:
437 | # If one of the moves leads to a better score than the current best
438 | # achievable score, then replace it with this one.
439 | alpha = val
440 | best_move = move
441 | return alpha, best_move
442 |
443 | def alphabeta_searcher(depth, evaluate):
444 | def strategy(player, board):
445 | return alphabeta(player, board, MIN_VALUE, MAX_VALUE, depth, evaluate)[1]
446 | return strategy
447 |
448 |
449 | # -----------------------------------------------------------------------------
450 | #
451 | ## Conclusion
452 |
453 | # The strategies we've discussed are very general and are applicable to a broad
454 | # range of strategy games, such as Chess, Checkers, and Go. More advanced
455 | # strategies for Othello exist that apply various gameplay heuristics; some of
456 | # these are discussed in "Paradigms of Artificial Intelligence Programming" by
457 | # Peter Norvig.
458 | #
459 | # See Wikipedia for more details on [minimax][mm] and [alpha-beta][ab] search.
460 | #
461 | # [mm]: http://en.wikipedia.org/wiki/Minimax
462 | # [ab]: http://en.wikipedia.org/wiki/Alpha-beta_pruning
463 |
--------------------------------------------------------------------------------
/paip/tests/test_emycin.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from paip.emycin import *
3 |
4 |
5 | def eq(x, y):
6 | return x == y
7 |
8 |
9 | class CFTests(unittest.TestCase):
10 | def test_cf_or(self):
11 | cases = [
12 | (0.6, 0.4, 0.76),
13 | (-0.3, -0.75, -0.825),
14 | (0.3, -0.4, -1.0/7.0),
15 | (-0.4, 0.3, -1.0/7.0),
16 | ]
17 | for a, b, c in cases:
18 | self.assertAlmostEqual(c, cf_or(a, b))
19 |
20 | def test_cf_and(self):
21 | cases = [
22 | (0.6, 0.4, 0.4),
23 | (-0.3, -0.75, -0.75),
24 | (0.3, -0.4, -0.4),
25 | (-0.4, 0.3, -0.4),
26 | ]
27 | for a, b, c in cases:
28 | self.assertAlmostEqual(c, cf_and(a, b))
29 |
30 | def test_is_cf(self):
31 | cases = [
32 | (-0.7, True),
33 | (0.0, True),
34 | (0.999, True),
35 | (1.001, False),
36 | (-3, False),
37 | ]
38 | for a, b in cases:
39 | self.assertEqual(b, is_cf(a))
40 |
41 | def test_cf_true(self):
42 | cases = [
43 | (-3, False),
44 | (-0.85, False),
45 | (-0.15, False),
46 | (0.0, False),
47 | (0.15, False),
48 | (0.999, True),
49 | (1.04, False),
50 | ]
51 | for a, b in cases:
52 | self.assertEqual(b, cf_true(a))
53 |
54 | def test_cf_false(self):
55 | cases = [
56 | (-3, False),
57 | (-0.85, True),
58 | (-0.15, False),
59 | (0.0, False),
60 | (0.15, False),
61 | (0.999, False),
62 | (1.04, False),
63 | ]
64 | for a, b in cases:
65 | self.assertEqual(b, cf_false(a))
66 |
67 |
68 | class ContextTests(unittest.TestCase):
69 | def test_instantiate(self):
70 | ctx = Context('patient')
71 | self.assertEqual(0, ctx.count)
72 | inst = ctx.instantiate()
73 | self.assertEqual(1, ctx.count)
74 | self.assertEqual(('patient', 0), inst)
75 |
76 |
77 | class ParameterTests(unittest.TestCase):
78 | def test_validate(self):
79 | age = Parameter('age', cls=lambda x: int(x))
80 | self.assertEqual(25, age.from_string('25'))
81 | self.assertRaises(ValueError, age.from_string, 'foo')
82 |
83 |
84 | class ConditionTests(unittest.TestCase):
85 | def test_eval_condition(self):
86 | condition = ('age', 'patient', lambda x, y: x < y, 25)
87 | values = dict([(22, 0.3), (27, -0.1), (24, 0.6)])
88 | self.assertAlmostEqual(0.9, eval_condition(condition, values))
89 |
90 |
91 | class ValuesTests(unittest.TestCase):
92 | def setUp(self):
93 | self.values = {
94 | ('age', ('patient', 0)): dict([(22, 0.3), (27, -0.1), (24, 0.6)]),
95 | ('health', ('patient', 0)): dict([('good', 0.8), ('moderate', -0.4)]),
96 | ('temp', ('weather', 347)): dict([(79, 0.3), (81, 0.4)]),
97 | ('temp', ('weather', 348)): dict([(79, 0.4), (80, -0.4)]),
98 | ('temp', ('weather', 349)): dict([(82, 0.6), (83, 0.05)]),
99 | ('happy', ('patient', 0)): dict([(True, 0.7)]),
100 | }
101 |
102 | def test_get_vals_empty(self):
103 | self.assertEqual(0, len(get_vals(self.values, 'happy', ('patient', 1)).keys()))
104 |
105 | def test_get_vals(self):
106 | self.assertEqual(3, len(get_vals(self.values, 'age', ('patient', 0)).keys()))
107 |
108 | def test_get_cf_none(self):
109 | self.assertEqual(CF.unknown, get_cf(self.values, 'age', ('patient', 0), 30))
110 |
111 | def test_get_cf(self):
112 | self.assertAlmostEqual(0.4, get_cf(self.values, 'temp', ('weather', 347), 81))
113 |
114 | def test_update_cf_none(self):
115 | update_cf(self.values, 'temp', ('weather', 347), 85, 0.3)
116 | self.assertAlmostEqual(0.3, get_cf(self.values, 'temp', ('weather', 347), 85))
117 |
118 | def test_update_cf(self):
119 | update_cf(self.values, 'temp', ('weather', 347), 81, 0.3)
120 | exp = cf_or(0.3, 0.4)
121 | self.assertAlmostEqual(exp, get_cf(self.values, 'temp', ('weather', 347), 81))
122 |
123 |
124 | class RuleTests(unittest.TestCase):
125 | def setUp(self):
126 | patient = ('patient', 0)
127 | weather1 = ('weather', 347)
128 | weather2 = ('weather', 348)
129 | weather3 = ('weather', 349)
130 | self.values = {
131 | ('age', patient): dict([(22, 0.3), (27, -0.1), (24, 0.6)]),
132 | ('health', patient): dict([('good', 0.8), ('moderate', -0.4)]),
133 | ('temp', weather1): dict([(79, 0.3), (81, 0.4)]),
134 | ('temp', weather2): dict([(79, 0.4), (80, -0.4)]),
135 | ('temp', weather3): dict([(82, 0.6), (83, 0.05)]),
136 | ('happy', patient): dict([(True, 0.7)]),
137 | }
138 | self.instances = {
139 | 'patient': patient,
140 | 'weather': weather1
141 | }
142 |
143 | def test_applicable_true(self):
144 | premises = [
145 | ('age', 'patient', lambda x, y: x < y, 25),
146 | ('health', 'patient', eq, 'good'),
147 | ('temp', 'weather', lambda x, y: x > y, 80)
148 | ]
149 | r = Rule(123, premises, [], 0)
150 | expected = cf_and(0.9, cf_and(0.4, 0.8))
151 | self.assertAlmostEqual(expected, r.applicable(self.values, self.instances))
152 |
153 | def test_applicable_false(self):
154 | premises = [
155 | ('age', 'patient', lambda x, y: x > y, 20),
156 | ('health', 'patient', eq, 'poor'),
157 | ('temp', 'weather', lambda x, y: x > y, 80)
158 | ]
159 | r = Rule(123, premises, [], 0)
160 | self.assertAlmostEqual(CF.false, r.applicable(self.values, self.instances))
161 |
162 | def test_apply_not_applicable(self):
163 | premises = [
164 | ('age', 'patient', lambda x, y: x > y, 20),
165 | ('health', 'patient', eq, 'poor'),
166 | ('temp', 'weather', lambda x, y: x > y, 80)
167 | ]
168 | conclusions = [
169 | ('dehydrated', 'patient', eq, False),
170 | ('happy', 'patient', eq, True),
171 | ]
172 |
173 | r = Rule(123, premises, conclusions, 0.9)
174 | r.apply(self.values, self.instances)
175 |
176 | cf = r.cf * r.applicable(self.values, self.instances)
177 | exp1, act1 = 0.7, get_cf(self.values, 'happy', ('patient', 0), True)
178 | exp2, act2 = CF.unknown, get_cf(self.values, 'dehydrated', ('patient', 0), False)
179 | self.assertAlmostEqual(exp1, act1)
180 | self.assertAlmostEqual(exp2, act2)
181 |
182 | def test_apply(self):
183 | premises = [
184 | ('age', 'patient', lambda x, y: x < y, 25),
185 | ('health', 'patient', eq, 'good'),
186 | ('temp', 'weather', lambda x, y: x > y, 80)
187 | ]
188 | conclusions = [
189 | ('dehydrated', 'patient', eq, False),
190 | ('happy', 'patient', eq, True),
191 | ]
192 |
193 | r = Rule(123, premises, conclusions, 0.9)
194 | r.apply(self.values, self.instances)
195 |
196 | cf = r.cf * r.applicable(self.values, self.instances)
197 | exp1, act1 = cf_or(cf, 0.7), get_cf(self.values, 'happy', ('patient', 0), True)
198 | exp2, act2 = cf, get_cf(self.values, 'dehydrated', ('patient', 0), False)
199 | self.assertAlmostEqual(exp1, act1)
200 | self.assertAlmostEqual(exp2, act2)
201 |
202 |
203 | class UseRulesTests(unittest.TestCase):
204 | def setUp(self):
205 | patient = ('patient', 0)
206 | weather1 = ('weather', 347)
207 | weather2 = ('weather', 348)
208 | weather3 = ('weather', 349)
209 | self.values = {
210 | ('age', patient): dict([(22, 0.3), (27, -0.1), (24, 0.6)]),
211 | ('health', patient): dict([('good', 0.8), ('moderate', -0.4)]),
212 | ('temp', weather1): dict([(79, 0.3), (81, 0.4)]),
213 | ('temp', weather2): dict([(79, 0.4), (80, -0.4)]),
214 | ('temp', weather3): dict([(82, 0.6), (83, 0.05)]),
215 | ('happy', patient): dict([(True, 0.7)]),
216 | }
217 | self.instances = {
218 | 'patient': patient,
219 | 'weather': weather3
220 | }
221 |
222 | def test_use_rules_fail(self):
223 | # should not be applied
224 | premises1 = [
225 | ('age', 'patient', lambda x, y: x > y, 20),
226 | ('health', 'patient', eq, 'poor'),
227 | ('temp', 'weather', lambda x, y: x > y, 80)
228 | ]
229 | conclusions1 = [
230 | ('happy', 'patient', eq, True),
231 | ]
232 | rule1 = Rule(123, premises1, conclusions1, 0.9)
233 |
234 | # should not be applied
235 | premises2 = [
236 | ('temp', 'weather', eq, 81)
237 | ]
238 | conclusions2 = [
239 | ('foo', 'bar', lambda x, y: True, 'baz')
240 | ]
241 | rule2 = Rule(456, premises2, conclusions2, 0.7)
242 |
243 | self.assertFalse(use_rules(self.values, self.instances, [rule1, rule2]))
244 |
245 | exp1 = 0.7
246 | self.assertAlmostEqual(exp1,
247 | get_cf(self.values, 'happy', ('patient', 0), True))
248 |
249 | exp2 = CF.unknown
250 | self.assertAlmostEqual(exp2, get_cf(self.values, 'foo', ('bar', 0), 'baz'))
251 |
252 | def test_use_rules(self):
253 | # should be applied
254 | premises1 = [
255 | ('age', 'patient', lambda x, y: x < y, 25),
256 | ('health', 'patient', eq, 'good'),
257 | ('temp', 'weather', lambda x, y: x > y, 80)
258 | ]
259 | conclusions1 = [
260 | ('dehydrated', 'patient', eq, False),
261 | ]
262 | rule1 = Rule(123, premises1, conclusions1, 0.9)
263 |
264 | # should NOT be applied
265 | premises2 = [
266 | ('age', 'patient', lambda x, y: x > y, 20),
267 | ('health', 'patient', eq, 'poor'),
268 | ('temp', 'weather', lambda x, y: x > y, 80)
269 | ]
270 | conclusions2 = [
271 | ('dehydrated', 'patient', eq, True),
272 | ]
273 | rule2 = Rule(456, premises2, conclusions2, 0.7)
274 |
275 | # should be applied
276 | premises3 = [
277 | ('age', 'patient', lambda x, y: x < y, 25),
278 | ('health', 'patient', eq, 'good'),
279 | ('temp', 'weather', lambda x, y: x > y, 80)
280 | ]
281 | conclusions3 = [
282 | ('health', 'patient', eq, 'poor')
283 | ]
284 | rule3 = Rule(789, premises3, conclusions3, 0.85)
285 |
286 | self.assertTrue(use_rules(self.values, self.instances, [rule1, rule2, rule3]))
287 |
288 | exp1 = rule1.cf * rule1.applicable(self.values, self.instances)
289 | self.assertAlmostEqual(exp1,
290 | get_cf(self.values, 'dehydrated', ('patient', 0), False))
291 |
292 | exp2 = CF.unknown
293 | self.assertAlmostEqual(exp2,
294 | get_cf(self.values, 'dehydrated', ('patient', 0), True))
295 |
296 | exp3 = rule3.cf * rule3.applicable(self.values, self.instances)
297 | self.assertAlmostEqual(exp3,
298 | get_cf(self.values, 'health', ('patient', 0), 'poor'))
299 |
300 |
301 | class ParseReplyTests(unittest.TestCase):
302 | def setUp(self):
303 | self.param = Parameter('age', cls=int)
304 |
305 | def test_parse_value(self):
306 | vals = parse_reply(self.param, '25')
307 | self.assertEqual(1, len(vals))
308 | val, cf = vals[0]
309 | self.assertEqual(25, val)
310 | self.assertAlmostEqual(CF.true, cf)
311 |
312 | def test_parse_list(self):
313 | vals = parse_reply(self.param, '25 0.4, 23 -0.2, 24 0.1')
314 | self.assertEqual(3, len(vals))
315 | val1, cf1 = vals[0]
316 | val2, cf2 = vals[1]
317 | val3, cf3 = vals[2]
318 | self.assertEqual(25, val1)
319 | self.assertEqual(23, val2)
320 | self.assertEqual(24, val3)
321 | self.assertAlmostEqual(0.4, cf1)
322 | self.assertAlmostEqual(-0.2, cf2)
323 | self.assertAlmostEqual(0.1, cf3)
324 |
325 | class ShellTests(unittest.TestCase):
326 | def setUp(self):
327 | sh = Shell()
328 | self.shell = sh
329 |
330 | # define contexts and parameters
331 |
332 | sh.define_context(Context('patient'))
333 | sh.define_param(Parameter('age', 'patient', cls=int))
334 | sh.define_param(Parameter('health', 'patient', enum=['good', 'ok', 'poor']))
335 | sh.define_param(Parameter('dehydrated', 'patient', cls=str))
336 | def boolean(x):
337 | if x == 'True':
338 | return True
339 | if x == 'False':
340 | return False
341 | raise ValueError('%s is not True or False' % x)
342 | sh.define_param(Parameter('awesome', 'patient', cls=boolean))
343 |
344 | sh.define_context(Context('weather'))
345 | sh.define_param(Parameter('temp', 'weather', cls=int))
346 |
347 | # define rules
348 |
349 | premises1 = [
350 | ('age', 'patient', lambda x, y: x < y, 25),
351 | ('health', 'patient', eq, 'good'),
352 | ('temp', 'weather', lambda x, y: x > y, 80)
353 | ]
354 | conclusions1 = [('dehydrated', 'patient', eq, False)]
355 | sh.define_rule(Rule(123, premises1, conclusions1, 0.9))
356 |
357 | premises2 = [
358 | ('age', 'patient', lambda x, y: x > y, 20),
359 | ('health', 'patient', eq, 'poor'),
360 | ('temp', 'weather', lambda x, y: x > y, 80)
361 | ]
362 | conclusions2 = [('dehydrated', 'patient', eq, True)]
363 | sh.define_rule(Rule(456, premises2, conclusions2, 0.7))
364 |
365 | premises3 = [
366 | ('age', 'patient', lambda x, y: x > y, 40),
367 | ('temp', 'weather', lambda x, y: x > y, 85)
368 | ]
369 | conclusions3 = [('health', 'patient', eq, 'poor')]
370 | sh.define_rule(Rule(789, premises3, conclusions3, 0.85))
371 |
372 | # make instances
373 | self.weather = self.shell.instantiate('weather')
374 | self.patient = self.shell.instantiate('patient')
375 |
376 | # fill in initial data
377 | update_cf(self.shell.known_values, 'age', self.patient, 45, 0.7)
378 | self.shell.known.add(('age', self.patient))
379 | update_cf(self.shell.known_values, 'temp', self.weather, 89, 0.6)
380 | self.shell.known.add(('temp', self.weather))
381 |
382 | def test_find_out_ask_first_then_use_rules_current_instance(self):
383 | self.shell.get_param('health').ask_first = True
384 | self.shell.read = lambda prompt: 'unknown'
385 | self.assertTrue(('health', self.patient) not in self.shell.known_values)
386 | self.assertTrue(('health', self.patient) not in self.shell.asked)
387 | self.shell.find_out('health')
388 | self.assertTrue(('health', self.patient) in self.shell.asked)
389 | self.assertTrue(('health', self.patient) in self.shell.known_values)
390 |
391 | def test_find_out_ask_first_success_current_instance(self):
392 | self.shell.get_param('health').ask_first = True
393 | self.shell.read = lambda prompt: 'good'
394 | self.assertTrue(('health', self.patient) not in self.shell.known_values)
395 | self.assertTrue(('health', self.patient) not in self.shell.asked)
396 | self.shell.find_out('health')
397 | self.assertTrue(('health', self.patient) in self.shell.asked)
398 | self.assertTrue(('health', self.patient) in self.shell.known_values)
399 |
400 | cf = get_cf(self.shell.known_values, 'health', self.patient, 'good')
401 | self.assertAlmostEqual(CF.true, cf)
402 |
403 | def test_find_out_rules_first_then_ask_current_instance(self):
404 | self.shell.get_param('awesome').ask_first = False
405 | self.shell.read = lambda prompt: 'True 0.7, False -0.4'
406 | self.assertTrue(('awesome', self.patient) not in self.shell.known_values)
407 | self.assertTrue(('awesome', self.patient) not in self.shell.asked)
408 | self.shell.find_out('awesome')
409 | self.assertTrue(('awesome', self.patient) in self.shell.asked)
410 | self.assertTrue(('awesome', self.patient) in self.shell.known_values)
411 |
412 | cf1 = get_cf(self.shell.known_values, 'awesome', self.patient, True)
413 | self.assertAlmostEqual(0.7, cf1)
414 | cf2 = get_cf(self.shell.known_values, 'awesome', self.patient, False)
415 | self.assertAlmostEqual(-0.4, cf2)
416 |
417 | def test_find_out_rules_first_current_instance(self):
418 | self.shell.get_param('health').ask_first = False
419 | self.assertTrue(('health', self.patient) not in self.shell.known_values)
420 | self.assertTrue(('health', self.patient) not in self.shell.asked)
421 | self.shell.find_out('health')
422 | self.assertTrue(('health', self.patient) not in self.shell.asked)
423 | self.assertTrue(('health', self.patient) in self.shell.known_values)
424 |
425 | def test_find_out_ask_first_then_use_rules(self):
426 | self.shell.get_param('health').ask_first = True
427 | self.shell.read = lambda prompt: 'unknown'
428 | self.assertTrue(('health', self.patient) not in self.shell.known_values)
429 | self.assertTrue(('health', self.patient) not in self.shell.asked)
430 | self.shell.find_out('health', self.patient)
431 | self.assertTrue(('health', self.patient) in self.shell.asked)
432 | self.assertTrue(('health', self.patient) in self.shell.known_values)
433 |
434 | def test_find_out_ask_first_success(self):
435 | self.shell.get_param('health').ask_first = True
436 | self.shell.read = lambda prompt: 'good'
437 | self.assertTrue(('health', self.patient) not in self.shell.known_values)
438 | self.assertTrue(('health', self.patient) not in self.shell.asked)
439 | self.shell.find_out('health', self.patient)
440 | self.assertTrue(('health', self.patient) in self.shell.asked)
441 | self.assertTrue(('health', self.patient) in self.shell.known_values)
442 |
443 | cf = get_cf(self.shell.known_values, 'health', self.patient, 'good')
444 | self.assertAlmostEqual(CF.true, cf)
445 |
446 | def test_find_out_rules_first_then_ask(self):
447 | self.shell.get_param('awesome').ask_first = False
448 | self.shell.read = lambda prompt: 'True 0.7, False -0.4'
449 | self.assertTrue(('awesome', self.patient) not in self.shell.known_values)
450 | self.assertTrue(('awesome', self.patient) not in self.shell.asked)
451 | self.shell.find_out('awesome', self.patient)
452 | self.assertTrue(('awesome', self.patient) in self.shell.asked)
453 | self.assertTrue(('awesome', self.patient) in self.shell.known_values)
454 |
455 | cf1 = get_cf(self.shell.known_values, 'awesome', self.patient, True)
456 | self.assertAlmostEqual(0.7, cf1)
457 | cf2 = get_cf(self.shell.known_values, 'awesome', self.patient, False)
458 | self.assertAlmostEqual(-0.4, cf2)
459 |
460 | def test_find_out_rules_first(self):
461 | self.shell.get_param('health').ask_first = False
462 | self.assertTrue(('health', self.patient) not in self.shell.known_values)
463 | self.assertTrue(('health', self.patient) not in self.shell.asked)
464 | self.shell.find_out('health', self.patient)
465 | self.assertTrue(('health', self.patient) not in self.shell.asked)
466 | self.assertTrue(('health', self.patient) in self.shell.known_values)
467 |
--------------------------------------------------------------------------------