Merge branch 'develop-0.2' of https://github.com/catseye/Samovar into propositions-query
Chris Pressey
6 years ago
2 | 2 | |
3 | 3 | *Version 0.2 (unreleased). Subject to change in backwards-incompatible ways.* |
4 | 4 | |
5 | Samovar is a DSL for world-modeling using propositions. | |
5 | Samovar is a DSL for modelling a world using propositions (facts), and possible | |
6 | events that can occur based on those facts, changing them. | |
6 | 7 | |
7 | A Samovar world is an immutable set of rules which operate on a mutable set of | |
8 | facts. Each rule looks like | |
8 | Possible events are described with _event rules_. Each event rule looks like | |
9 | 9 | |
10 | 10 | [A] X [B] |
11 | 11 | |
27 | 27 | |
28 | 28 | [actor(α),item(β),holding(α,β)] α puts down the β. [~holding(α,β)] |
29 | 29 | |
30 | And we can package this all into a world-description: | |
30 | And we can package this all into a scenario: | |
31 | 31 | |
32 | rules | |
33 | [actor(α),item(β),~holding(α,β)] α picks up the β. [holding(α,β)] | |
34 | [actor(α),item(β),holding(α,β)] α puts down the β. [~holding(α,β)] | |
35 | end | |
36 | situations | |
37 | [actor(Ignatz),item(brick)] | |
38 | end | |
32 | scenario IgnatzWithBrick { | |
33 | ||
34 | [actor(α),item(β),~holding(α,β)] α picks up the β. [holding(α,β)] | |
35 | [actor(α),item(β),holding(α,β)] α puts down the β. [~holding(α,β)] | |
36 | ||
37 | actor(Ignatz). | |
38 | item(brick). | |
39 | ||
40 | goal []. | |
41 | } | |
39 | 42 | |
40 | And an implementation of Samovar could take this world-description and use it to, | |
43 | And an implementation of Samovar could take this scenario and use it to, | |
41 | 44 | among other things, generate textual descriptions of chains of events like |
42 | 45 | |
43 | 46 | Ignatz picks up the brick. Ignatz puts down the brick. |
63 | 66 | |
64 | 67 | * Maybe allow `∨` - there doesn't seem to be as much call for it, though. |
65 | 68 | * Allow sentence trees to be given for actions. |
66 | * Allow situations to define a termination condition, so that the implementation | |
67 | can generate a scenario where the condition is met (by whatever method). | |
69 | * Allow scenarios to define a termination condition, so that the implementation | |
70 | can generate a scene where the condition is met (by whatever method). | |
71 | * Allow scenarios to specify a minimum number of events to generate (maybe?) | |
68 | 72 | * Consider what it would take to add a predicate that evaluates to whether |
69 | 73 | a given action has been taken previously or not. |
40 | 40 | help="Run cProfile on standard 'heavy load' case and exit") |
41 | 41 | optparser.add_option("--words", |
42 | 42 | type=int, default=0, |
43 | help="If given, generate each situation til this many words") | |
43 | help="If given, generate each scenario til this many words") | |
44 | 44 | (options, args) = optparser.parse_args(sys.argv[1:]) |
45 | 45 | |
46 | 46 | if options.profile: |
68 | 68 | sys.exit(0) |
69 | 69 | if options.seed is not None: |
70 | 70 | random.seed(options.seed) |
71 | g = Generator(ast, debug=options.debug) | |
72 | if options.length > 0: | |
73 | events = g.generate_events(options.length) | |
74 | elif options.words > 0: | |
75 | events = g.generate_words(options.words) | |
76 | for e in events: | |
77 | if options.line_per_sentence: | |
78 | sys.stdout.write("%s\n" % e) | |
79 | else: | |
80 | sys.stdout.write("%s " % e) | |
81 | sys.stdout.write("\n") | |
71 | for scenario in ast.scenarios: | |
72 | if scenario.goal is None: | |
73 | continue | |
74 | g = Generator(ast, scenario, debug=options.debug) | |
75 | if options.length > 0: | |
76 | events = g.generate_events(options.length) | |
77 | elif options.words > 0: | |
78 | events = g.generate_words(options.words) | |
79 | for e in events: | |
80 | if options.line_per_sentence: | |
81 | sys.stdout.write("%s\n" % e) | |
82 | else: | |
83 | sys.stdout.write("%s " % e) | |
84 | sys.stdout.write("\n") |
11 | 11 | -> shell command |
12 | 12 | -> "bin/samovar %(test-body-file) --words 20 --line-per-sentence --seed 0" |
13 | 13 | |
14 | Ignatz | |
15 | ------ | |
14 | Scenarios | |
15 | --------- | |
16 | 16 | |
17 | rules | |
17 | The basic unit of a Samovar world is a scenario. A scenario may contain | |
18 | any number of propositions and event rules, and an optional goal, in any order. | |
19 | ||
20 | scenario IgnatzWithBrick { | |
21 | ||
18 | 22 | [actor(α),item(β),~holding(α,β)] α picks up the β. [holding(α,β)] |
19 | 23 | [actor(α),item(β),holding(α,β)] α puts down the β. [~holding(α,β)] |
20 | end | |
21 | situations | |
22 | [actor(Ignatz),item(brick)] | |
23 | end | |
24 | ||
25 | actor(Ignatz). | |
26 | item(brick). | |
24 | 27 | |
28 | goal []. | |
29 | } | |
25 | 30 | ===> Ignatz picks up the brick. |
26 | 31 | ===> Ignatz puts down the brick. |
27 | 32 | ===> Ignatz picks up the brick. |
28 | 33 | ===> Ignatz puts down the brick. |
29 | 34 | |
35 | A source file may contain more than one scenario. By default, our | |
36 | implementation of Samovar runs a simulation on each of the scenarios | |
37 | that has a goal defined, even if that goal is empty. | |
38 | ||
39 | scenario MollyWithBrick { | |
40 | ||
41 | [actor(α),item(β),~holding(α,β)] α picks up the β. [holding(α,β)] | |
42 | [actor(α),item(β),holding(α,β)] α puts down the β. [~holding(α,β)] | |
43 | ||
44 | actor(Molly). | |
45 | item(brick). | |
46 | ||
47 | } | |
48 | ||
49 | scenario IgnatzWithBrick { | |
50 | ||
51 | [actor(α),item(β),~holding(α,β)] α picks up the β. [holding(α,β)] | |
52 | [actor(α),item(β),holding(α,β)] α puts down the β. [~holding(α,β)] | |
53 | ||
54 | actor(Ignatz). | |
55 | item(brick). | |
56 | ||
57 | goal []. | |
58 | } | |
59 | ===> Ignatz picks up the brick. | |
60 | ===> Ignatz puts down the brick. | |
61 | ===> Ignatz picks up the brick. | |
62 | ===> Ignatz puts down the brick. | |
63 | ||
64 | Scenarios can import the event rules and propositions from other scenarios. | |
65 | This makes a scenario a good place to collect a setting, or a group of | |
66 | characters who will appear together in scenes. These "library" scenarios | |
67 | should have no goal, as we don't want to generate simulations for them. | |
68 | ||
69 | scenario ItemRules { | |
70 | [actor(α),item(β),~holding(α,β)] α picks up the β. [holding(α,β)] | |
71 | [actor(α),item(β),holding(α,β)] α puts down the β. [~holding(α,β)] | |
72 | } | |
73 | scenario Actors { | |
74 | actor(Ignatz). | |
75 | } | |
76 | scenario Brickyard { | |
77 | item(brick). | |
78 | } | |
79 | scenario Main { | |
80 | import ItemRules. | |
81 | import Actors. | |
82 | import Brickyard. | |
83 | goal []. | |
84 | } | |
85 | ===> Ignatz picks up the brick. | |
86 | ===> Ignatz puts down the brick. | |
87 | ===> Ignatz picks up the brick. | |
88 | ===> Ignatz puts down the brick. | |
89 | ||
90 | There is nothing stopping an implementation from allowing a Samovar | |
91 | description to be spread over multiple source files, but there is no | |
92 | facility to reference one source file from another in Samovar, so how | |
93 | they are located and collected is up to the implementation. | |
30 | 94 | |
31 | 95 | chairs |
32 | 96 | ------ |
33 | 97 | |
34 | rules | |
98 | scenario Chairs { | |
35 | 99 | |
36 | 100 | [actor(ρ)∧¬sitting(ρ)] |
37 | 101 | ρ walks around the room. |
49 | 113 | ρ gets up and stretches. |
50 | 114 | [¬sitting(ρ)∧¬in(ρ,κ)∧empty(κ)] |
51 | 115 | |
52 | end | |
116 | actor(Hastings). | |
117 | actor(Petersen). | |
118 | actor(Wembley). | |
119 | nearby(chair). empty(chair). | |
120 | nearby(recliner). | |
121 | empty(recliner). | |
122 | nearby(sofa). | |
123 | empty(sofa). | |
53 | 124 | |
54 | situations | |
55 | ||
56 | [ | |
57 | actor(Hastings), | |
58 | actor(Petersen), | |
59 | actor(Wembley), | |
60 | nearby(chair), empty(chair), | |
61 | nearby(recliner), empty(recliner), | |
62 | nearby(sofa), empty(sofa) | |
63 | ] | |
64 | ||
65 | end | |
125 | goal []. | |
126 | } | |
66 | 127 | ===> Hastings sits down in the chair. |
67 | 128 | ===> Hastings leans back in the chair. |
68 | 129 | ===> Petersen sits down in the sofa. |
69 | 130 | ===> Wembley sits down in the recliner. |
70 | 131 | |
71 | idle | |
72 | ---- | |
73 | ||
74 | rules | |
75 | ||
76 | [actor(ρ)] | |
77 | ρ rubs his chin. | |
78 | [] | |
79 | ||
80 | [actor(ρ)] | |
81 | ρ yawns. | |
82 | [] | |
83 | ||
84 | end | |
85 | ???> IndexError | |
86 | 132 | |
87 | 133 | no need for functions |
88 | 134 | --------------------- |
96 | 142 | |
97 | 143 | but we can just say |
98 | 144 | |
99 | rules | |
145 | scenario ScratchesHead { | |
146 | ||
100 | 147 | [actor(ρ),possessive(ρ,ξ)] |
101 | 148 | ρ scratches ξ head. |
102 | 149 | [] |
103 | end | |
104 | situations | |
105 | [ | |
106 | actor(Alice), | |
107 | possessive(Alice, her), | |
108 | actor(Bob), | |
109 | possessive(Bob, his) | |
110 | ] | |
111 | end | |
112 | 150 | |
113 | ==> Bob scratches his head. | |
114 | ==> Bob scratches his head. | |
115 | ==> Alice scratches her head. | |
116 | ==> Alice scratches her head. | |
117 | ==> Bob scratches his head. | |
151 | actor(Alice). | |
152 | possessive(Alice, her). | |
153 | actor(Bob). | |
154 | possessive(Bob, his). | |
155 | ||
156 | goal []. | |
157 | } | |
158 | ===> Bob scratches his head. | |
159 | ===> Bob scratches his head. | |
160 | ===> Alice scratches her head. | |
161 | ===> Alice scratches her head. | |
162 | ===> Bob scratches his head. | |
118 | 163 | |
119 | 164 | This loses the nice property of the function name being a readable |
120 | 165 | placeholder in the sentence, but you can now use named variables |
121 | 166 | instead: |
122 | 167 | |
123 | rules | |
168 | scenario ScratchesHead { | |
169 | ||
124 | 170 | [actor(?Actor),possessive(?Actor,?their)] |
125 | 171 | ?Actor scratches ?their head. |
126 | 172 | [] |
127 | end | |
128 | situations | |
129 | [ | |
130 | actor(Alice), | |
131 | possessive(Alice, her), | |
132 | actor(Bob), | |
133 | possessive(Bob, his) | |
134 | ] | |
135 | end | |
136 | 173 | |
137 | ==> Bob scratches his head. | |
138 | ==> Bob scratches his head. | |
139 | ==> Alice scratches her head. | |
140 | ==> Alice scratches her head. | |
141 | ==> Bob scratches his head. | |
174 | actor(Alice). | |
175 | possessive(Alice, her). | |
176 | actor(Bob). | |
177 | possessive(Bob, his). | |
178 | ||
179 | goal []. | |
180 | } | |
181 | ===> Bob scratches his head. | |
182 | ===> Bob scratches his head. | |
183 | ===> Alice scratches her head. | |
184 | ===> Alice scratches her head. | |
185 | ===> Bob scratches his head. |
0 | rules | |
0 | scenario Chairs { | |
1 | 1 | |
2 | [actor(ρ)∧¬sitting(ρ)] | |
3 | ρ walks around the room. | |
4 | [] | |
2 | [actor(ρ)∧¬sitting(ρ)] | |
3 | ρ walks around the room. | |
4 | [] | |
5 | 5 | |
6 | [actor(ρ)∧¬sitting(ρ)∧nearby(κ)∧empty(κ)] | |
7 | ρ sits down in the κ. | |
8 | [sitting(ρ)∧in(ρ,κ)∧¬empty(κ)] | |
6 | [actor(ρ)∧¬sitting(ρ)∧nearby(κ)∧empty(κ)] | |
7 | ρ sits down in the κ. | |
8 | [sitting(ρ)∧in(ρ,κ)∧¬empty(κ)] | |
9 | 9 | |
10 | [actor(ρ)∧sitting(ρ)∧in(ρ,κ)] | |
11 | ρ leans back in the κ. | |
12 | [] | |
10 | [actor(ρ)∧sitting(ρ)∧in(ρ,κ)] | |
11 | ρ leans back in the κ. | |
12 | [] | |
13 | 13 | |
14 | [actor(ρ)∧sitting(ρ)∧in(ρ,κ)] | |
15 | ρ gets up and stretches. | |
16 | [¬sitting(ρ)∧¬in(ρ,κ)∧empty(κ)] | |
14 | [actor(ρ)∧sitting(ρ)∧in(ρ,κ)] | |
15 | ρ gets up and stretches. | |
16 | [¬sitting(ρ)∧¬in(ρ,κ)∧empty(κ)] | |
17 | 17 | |
18 | end | |
18 | actor(Hastings). | |
19 | actor(Petersen). | |
20 | actor(Wembley). | |
21 | nearby(chair), empty(chair). | |
22 | nearby(recliner), empty(recliner). | |
23 | nearby(sofa), empty(sofa). | |
19 | 24 | |
20 | situations | |
21 | ||
22 | [ | |
23 | actor(Hastings), | |
24 | actor(Petersen), | |
25 | actor(Wembley), | |
26 | nearby(chair), empty(chair), | |
27 | nearby(recliner), empty(recliner), | |
28 | nearby(sofa), empty(sofa) | |
29 | ] | |
30 | ||
31 | end | |
25 | goal []. | |
26 | } |
0 | rules | |
1 | ||
2 | [actor(ρ)] | |
3 | ρ scratches their(ρ) head. | |
4 | [] | |
5 | ||
6 | end | |
7 | ||
8 | functions | |
9 | ||
10 | their(Alice) → her | |
11 | their(Bob) → his | |
12 | ||
13 | end | |
14 | ||
15 | situations | |
16 | ||
17 | [ | |
18 | actor(Alice), | |
19 | actor(Bob) | |
20 | ] | |
21 | ||
22 | end |
0 | rules | |
0 | scenario HeadScratching { | |
1 | ||
1 | 2 | [actor(?Actor),possessive(?Actor,?their)] |
2 | 3 | ?Actor scratches ?their head. |
3 | 4 | [] |
4 | end | |
5 | situations | |
6 | [ | |
7 | actor(Alice), | |
8 | possessive(Alice, her), | |
9 | actor(Bob), | |
10 | possessive(Bob, his) | |
11 | ] | |
12 | end | |
5 | ||
6 | actor(Alice). | |
7 | possessive(Alice, her). | |
8 | actor(Bob). | |
9 | possessive(Bob, his). | |
10 | ||
11 | goal []. | |
12 | } |
13 | 13 | |
14 | 14 | |
15 | 15 | class Generator(object): |
16 | def __init__(self, world, debug=False): | |
16 | def __init__(self, world, scenario, debug=False): | |
17 | 17 | self.world = world |
18 | 18 | self.debug = debug |
19 | 19 | self.state = set() # set of things currently true about the world |
20 | for e in self.world.situations[0].cond.exprs: | |
21 | if isinstance(e, Assert): | |
22 | self.state.add(e.term) | |
20 | self.scenario = scenario | |
21 | for term in self.scenario.propositions: | |
22 | self.state.add(term) | |
23 | 23 | |
24 | 24 | def generate_events(self, count): |
25 | 25 | if self.debug: |
49 | 49 | |
50 | 50 | def get_candidate_rules(self): |
51 | 51 | candidates = [] |
52 | for rule in self.world.rules: | |
52 | for rule in self.scenario.rules: | |
53 | 53 | for unifier in match_all(self.state, rule.pre.exprs, {}): |
54 | 54 | candidates.append((rule, unifier)) |
55 | 55 |
0 | 0 | # encoding: UTF-8 |
1 | 1 | |
2 | from samovar.ast import World, Rule, Situation, Cond, Assert, Retract | |
2 | from samovar.ast import World, Scenario, Rule, Cond, Assert, Retract | |
3 | 3 | from samovar.terms import Term, Var |
4 | 4 | from samovar.scanner import Scanner |
5 | 5 | |
6 | 6 | |
7 | # World ::= {Rules | Situations}. | |
8 | # Rules ::= "rules" {Rule} "end". | |
9 | # Situations ::= "situations" {Situation} "end". | |
7 | # World ::= {Scenario}. | |
8 | # Scenario ::= "scenario" Atom "{" {Import | Proposition | Rule | Goal} ["." | ","] "}". | |
9 | # Import ::= "import" Atom. | |
10 | # Goal ::= "goal" Cond. | |
11 | # Proposition ::= Term. | |
10 | 12 | # Rule ::= Cond {Term | Punct} Cond. |
11 | # Situation ::= Cond. | |
12 | 13 | # Cond ::= "[" Expr {"," Expr} "]". |
13 | 14 | # Expr ::= Term | NotSym Term. |
14 | 15 | # Term ::= Var | Atom ["(" Term {AndSym Term} ")"]. |
23 | 24 | class Parser(object): |
24 | 25 | def __init__(self, text): |
25 | 26 | self.scanner = Scanner(text) |
27 | self.scenario_map = {} | |
26 | 28 | |
27 | 29 | def world(self): |
30 | scenarios = [] | |
31 | while self.scanner.on('scenario'): | |
32 | scenario = self.scenario() | |
33 | self.scenario_map[scenario.name] = scenario | |
34 | scenarios.append(scenario) | |
35 | return World(scenarios=scenarios) | |
36 | ||
37 | def scenario(self): | |
38 | propositions = [] | |
28 | 39 | rules = [] |
29 | situations = [] | |
30 | while self.scanner.on('rules', 'situations'): | |
31 | if self.scanner.on('rules'): | |
32 | rules.extend(self._section('rules', self.rule)) | |
33 | if self.scanner.on('situations'): | |
34 | situations.extend(self._section('situations', self.situation)) | |
35 | return World(rules=rules, situations=situations) | |
40 | goal = None | |
41 | self.scanner.expect('scenario') | |
42 | self.scanner.check_type('word') | |
43 | name = self.scanner.token | |
44 | self.scanner.scan() | |
45 | self.scanner.expect('{') | |
46 | while not self.scanner.on('}'): | |
47 | if self.scanner.consume('import'): | |
48 | self.scanner.check_type('word') | |
49 | from_name = self.scanner.token | |
50 | self.scanner.scan() | |
51 | from_scenario = self.scenario_map[from_name] | |
52 | rules.extend(from_scenario.rules) | |
53 | propositions.extend(from_scenario.propositions) | |
54 | elif self.scanner.consume('goal'): | |
55 | assert goal is None | |
56 | goal = self.cond() | |
57 | elif self.scanner.on('['): | |
58 | rules.append(self.rule()) | |
59 | else: | |
60 | propositions.append(self.proposition()) | |
61 | self.scanner.consume('.') | |
62 | self.scanner.consume(',') | |
63 | self.scanner.expect('}') | |
64 | return Scenario(name=name, propositions=propositions, rules=rules, goal=goal) | |
36 | 65 | |
37 | def _section(self, heading, method): | |
38 | items = [] | |
39 | self.scanner.expect(heading) | |
40 | while not self.scanner.on('end'): | |
41 | items.append(method()) | |
42 | self.scanner.expect('end') | |
43 | return items | |
66 | def proposition(self): | |
67 | return self.term() | |
44 | 68 | |
45 | 69 | def rule(self): |
46 | 70 | terms = [] |
49 | 73 | terms.append(self.term()) |
50 | 74 | post = self.cond() |
51 | 75 | return Rule(pre=pre, terms=terms, post=post) |
52 | ||
53 | def situation(self): | |
54 | cond = self.cond() | |
55 | return Situation(cond=cond) | |
56 | 76 | |
57 | 77 | def cond(self): |
58 | 78 | exprs = [] |
33 | 33 | self.token = None |
34 | 34 | self.type = 'EOF' |
35 | 35 | return |
36 | if self.scan_pattern(ur'\~|→|¬|∧|∨', 'operator'): | |
36 | if self.scan_pattern(ur'\~|→|=|¬|∧|∨', 'operator'): | |
37 | 37 | return |
38 | 38 | # TODO: not sure about the ? overloading (here and in punct). should be okay though? |
39 | 39 | if self.scan_pattern(r'\?[a-zA-Z_]+', 'qmark'): |