-
Notifications
You must be signed in to change notification settings - Fork 29
/
world_model.py
479 lines (433 loc) · 20.4 KB
/
world_model.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
'World and Concept classes for instantiaion by interactive fictions.'
__author__ = 'Nick Montfort'
__copyright__ = 'Copyright 2011 Nick Montfort'
__license__ = 'ISC'
__version__ = '0.5.0.0'
__status__ = 'Development'
import copy
import operator
import can
import item_model
def check_for_reserved_tags(items):
'Raise an error if a reserved tag, such as @cosmos, is in the list.'
if '@cosmos' in items:
raise StandardError('The tag "@cosmos" is reserved for the ' +
'special item at the root of the item tree. Use a different ' +
'tag for item now tagged "@cosmos".')
if '@focalizer' in items:
raise StandardError('The tag "@focalizer" is reserved for use ' +
'in indicating the actor who is currently focalizing the ' +
'narration. Use a different tag for item now tagged ' +
'"@focalizer".')
if '@commanded' in items:
raise StandardError('The tag "@commanded" is reserved for use ' +
'in indicating the actor who is currently being commanded. ' +
'Use a different tag for item now tagged "@commanded".')
class WorldOrConcept(object):
'Abstract base class for the World and for Concepts.'
def __init__(self, item_list, actions):
if self.__class__ == WorldOrConcept:
raise StandardError('Attempt to instantiate abstract base ' +
'class world_model.WorldOrConcept')
self.item = {}
self.act = actions
self.ticks = 0
seen_tags = []
# Construct the World's Item dictionary from the Item list:
for item in item_list:
if str(item) in seen_tags:
raise StandardError('The tag "' + str(item) + '" is ' +
"given to more than one item in the fiction's code. " +
'Item tags must be unique.')
seen_tags.append(str(item))
self.item[str(item)] = item
check_for_reserved_tags(self.item)
def __str__(self):
return str(self.act) + '\n' + str(self.item)
def accessible(self, actor):
'List all Items an Item can access.'
if actor == '@cosmos':
return self.item.keys()
compartment = self.compartment_of(actor)
tag_list = [str(compartment)]
for (link, child) in compartment.children:
if not link == 'on':
tag_list += [child]
tag_list += self.descendants(child, stop='closed')
tag_list += compartment.shared + self.doors(str(compartment))
accessible_list = []
for tag in tag_list:
if (not hasattr(self.item[tag], 'accessible') or
self.item[tag].accessible):
accessible_list.append(tag)
return accessible_list
def ancestors(self, tag):
'List all Items hierarchically above an Item.'
items_above = []
i = self.item[tag].parent
while i is not None:
items_above += [i]
i = self.item[i].parent
return items_above
def compartment_of(self, tag):
'Return the opaque compartment around the Item.'
if tag == '@cosmos' or self.item[tag].room:
return self.item[tag]
compartment = self.item[self.item[tag].parent]
while not (compartment.room or compartment.door or
str(compartment) == '@cosmos' or
(not compartment.transparent and
hasattr(compartment, 'open') and not compartment.open)):
# Keep ascending to the next parent until we encounter either
# (1) a room, (2) @cosmos, or (3) an opaque Item that has the "open"
# feature and is closed.
compartment = self.item[compartment.parent]
return compartment
def respondents(self, action):
"""Return a list: the cosmos, the Room of the agent, (living) contents.
These are all the Items that can prevent or react to an Action by
the agent of the action. If the Item has an "alive" feature, it is only
added if alive is True.
A special case: If the agent has congifured itself to new Room, the new
room and (living) contents have a chance to respond, too."""
tag_list = []
tag_list.append('@cosmos')
room = self.room_of(action.agent)
if room is not None:
tag_list.append(str(room))
for tag in self.descendants(str(room)):
if (not hasattr(self.item[tag], 'alive') or
self.item[tag].alive):
tag_list.append(tag)
if action.configure and action.direct == action.agent:
new_room = self.room_of(action.new_parent)
if not room == new_room and new_room is not None:
tag_list.append(str(new_room))
for tag in self.descendants(str(new_room)):
if (not hasattr(self.item[tag], 'alive') or
self.item[tag].alive):
tag_list.append(tag)
return tag_list
def descendants(self, tag, stop='bottom'):
"""List all Items hierarchically under "tag".
If stop='bottom', descend all the way. If stop='closed', go to down to
closed children, but not inside those; for stop='opaque', stop at
opaque ones."""
items_under = []
if (stop == 'bottom' or
(stop == 'closed' and (not hasattr(self.item[tag], 'open')
or self.item[tag].open)) or
(stop == 'opaque' and (not hasattr(self.item[tag], 'open')
or self.item[tag].open or self.item[tag].transparent))):
for (_, child) in self.item[tag].children:
if child in self.item:
items_under += [child] + self.descendants(child, stop=stop)
# If this is a room, include doors & shared things; otherwise [].
return items_under + self.item[tag].shared + self.doors(tag)
def has(self, category, tag):
'Does the tag represent an Item of this category in this World/Concept?'
return tag in self.item and getattr(self.item[tag], category)
def room_of(self, tag):
'If the Item exists and is in a Room, return the Room.'
while (tag in self.item and not self.has('room', tag) and
not self.has('door', tag) and not tag == '@cosmos'):
tag = self.item[tag].parent
if self.has('room', tag) or self.has('door', tag):
return self.item[tag]
return None
def show_descendants(self, tag, padding=''):
'Return the tree rooted at this Item.'
if tag not in self.item:
return ''
link = ''
if not tag == '@cosmos':
link = self.item[tag].link
string = (padding + tag + ': ' + self.item[tag].noun_phrase() +
' [' + link + ']\n')
for (_, child) in self.item[tag].children:
string += self.show_descendants(child, padding + (' '))
return string
def doors(self, tag):
"Returns a list of the Item's Doors; [] if there are none."
doors = []
if tag in self.item and self.item[tag].room:
for direction in self.item[tag].exits:
leads_to = self.item[tag].exits[direction]
if self.has('door', leads_to) and not leads_to in doors:
doors.append(leads_to)
return doors
class Concept(WorldOrConcept):
"An Actor's theory or model of the World, which can be used in telling."
def __init__(self, item_list, actions, cosmos=None):
self.changed = []
WorldOrConcept.__init__(self, item_list, actions)
if cosmos is None:
cosmos = item_model.Actor('@cosmos', called='nature',
allowed=can.have_any_item)
self.item['@cosmos'] = cosmos
for (tag, item) in self.item.items():
if not tag == '@cosmos':
self.item[item.parent].add_child(item.link, tag, True)
def item_at(self, tag, time):
'Return the Item from this moment in the Concept.'
if tag not in self.item:
return None
item = self.item[tag]
current = len(self.changed) - 1
while current >= 0 and self.changed[current][0] > time:
(_, changed_tag, old) = self.changed[current]
if changed_tag == tag:
item = old
current -= 1
return item
def update_item(self, item, time):
'After perception, change an Item within this Concept.'
if str(item) in self.item:
old = self.item[str(item)]
else:
old = None
self.item[str(item)] = item
self.changed.append((time, str(item), old))
def roll_back_to(self, time):
'Go back to a previous state of this Concept.'
new_ids = []
for action_id in self.act:
new_ids.append((action_id, self.act[action_id].start))
ids_times = sorted(new_ids, key=operator.itemgetter(1))
while len(ids_times) > 0 and ids_times[-1][1] > time:
(last_id, _) = ids_times.pop()
self.act.pop(last_id)
while len(self.changed) > 0 and self.changed[-1][0] > time:
(_, tag, old) = self.changed.pop()
if old is None:
del self.item[tag]
else:
self.item[tag] = old
def copy_at(self, time):
'Return a new Concept based on this one, but from an earlier time.'
new_concept = copy.deepcopy(self)
new_concept.roll_back_to(time)
return new_concept
def sight_culprit(prominence, view, lit):
'Which of the three factors is mostly to blame for the lack of visibility?'
if lit <= prominence and lit <= view:
return 'enough_light'
if view <= prominence and view <= lit:
return 'good_enough_view'
return 'item_prominent_enough'
class World(WorldOrConcept):
'The simulated world; it has Items and Actions.'
def __init__(self, fiction):
self.running = True
action_dict = {}
for action in fiction.initial_actions:
action.cause = 'initial_action'
action_dict[action.id] = action
self.concept = {}
WorldOrConcept.__init__(self, fiction.items, action_dict)
# Instantiate the needed amounts of Substance
for substance in [i for i in fiction.items if i.substance]:
parents = []
for tag in self.item:
if (hasattr(self.item[tag], 'source') and
self.item[tag].source == str(substance)):
parents.append(tag)
elif hasattr(self.item[tag], 'vessel'):
if self.item[tag].vessel == str(substance):
# The amount should go into the vessel itself.
parents.append(tag)
else:
# The amount should become the child of the main
# Substance Item, which is of @cosmos. It's necessary
# to create one amount for each empty vessel (or
# vessel that is holding something else) since that
# vessel might hold the Substance later.
parents.append(str(substance))
tag_number = 1
for parent in parents:
new_item = copy.deepcopy(substance)
new_item._tag += '_' + str(tag_number)
tag_number += 1
new_item.link = 'in'
new_item.parent = parent
self.item[str(new_item)] = new_item
if fiction.cosmos is None:
fiction.cosmos = item_model.Actor('@cosmos', called='nature',
allowed=can.have_any_item)
self.item['@cosmos'] = fiction.cosmos
for (tag, item) in self.item.items():
if not tag == '@cosmos':
self.item[item.parent].add_child(item.link, tag, True)
def advance_clock(self, duration):
'Move the time forward a specified number of ticks.'
self.ticks += duration
for actor in self.concept:
self.concept[actor].ticks = self.ticks
def back_up_clock(self, target_time):
'Roll the time back to a particuar tick.'
self.ticks = target_time
for actor in self.concept:
self.concept[actor].roll_back_to(self.ticks)
def light_level(self, tag):
"Determines the light level (not just glow) in the Item's compartment."
compartment = self.compartment_of(tag)
if compartment is None:
return 0.0
total = compartment.glow
for (_, child) in compartment.children:
total += self.light_within(child)
return total
def light_within(self, tag):
'Returns the light illuminating an Item, inherently and within.'
total = self.item[tag].glow # The inherent light coming from the item.
for (link, child) in self.item[tag].children:
if link == 'in':
# For Items that are 'in', descend if open or transparent.
if not hasattr(self.item[tag], 'open') or self.item[tag].open:
total += self.light_within(child)
elif self.item[tag].transparent:
total += self.light_within(child)
else:
total += self.light_within(child)
return total
def prevents_sight(self, actor, tag):
'Returns a reason (if there are any) that "actor" cannot see "tag".'
if actor == '@cosmos':
# @cosmos can see everything at all times.
return None
item_place = self.room_of(tag)
actor_place = self.room_of(actor)
if actor_place is None:
# The Actor is "out of play" (of @cosmos), and cannot see anything.
return 'actor_in_play'
if (item_place is None and
not tag in self.item[str(actor_place)].shared and
not tag in self.doors(str(actor_place))):
# The Item could be either a SharedThing or a Door if its Room is
# None. If its Room is None and neither is the case, however, it
# must be "out of play."
return 'item_in_play'
compartment = self.compartment_of(actor)
view_tags = []
if not compartment == actor_place:
# The Actor is is some sort of opaque compartment within a room.
# Only Items within that compartment will be visible.
view_tags = [str(compartment)]
for (link, child) in compartment.children:
if not link == 'on':
view_tags += [child]
view_tags += self.descendants(child, stop='opaque')
else:
# Otherwise, list all the Items to which there is a line of sight
# in the Actor's Room and in every Room that has a view from there.
if self.item[str(actor_place)].door:
rooms_visible = self.item[str(actor_place)].connects
else:
rooms_visible = actor_place.view.keys()
for room_tag in [str(actor_place)] + rooms_visible:
view_tags += ([room_tag] +
self.descendants(room_tag, stop='opaque'))
if tag not in view_tags:
return 'line_of_sight'
view = 1.0
# Set the view to be perfect (1.0). This applies if the Actor and
# Item are in the same Room, or if the Item is a SharedThing or Door
# of the Actor's Room, or if the Actor is in a Door and the Item is
# in a connecting Room.
if actor_place.room and str(item_place) in actor_place.view:
# If looking onto a Room in view, check how well it can be seen.
(view, _) = actor_place.view[str(item_place)]
lit = self.light_level(tag)
if str(self.compartment_of(actor)) == tag:
# The compartment itself is the one case where it's important to
# get the interior light level. If the line of sight crosses the
# compartment, the light level doesn't matter; the item can't be seen.
# If inside, the light level is computed within the compartment. But
# the compartment itself has different "inside" and "outside" light
# levels. Select an arbitrary child of the compartment (there must
# be at least one, the Actor) and check the light level for that child.
(_, child) = self.item[tag].children[0]
lit = self.light_level(child)
visibility = self.item[tag].prominence * view * lit
if visibility >= 0.2:
# 0.2 is the threshhold for seeing something.
# An actor sees something that has prominence 0.5 and is in a room with
# view 0.5 under full light (1): 0.5 * 0.5 * 1 = 0.25 >= 0.2
# An actor sees something that has prominece 1 and is in the same room
# even under very low (0.2) light: 1 * 1 * .02 = 0.2 >= 0.2
# Something with prominence below 0.2 will never be visible to an actor
# in the current system.
# Something with prominence 0.3 will be seen (under full light) in the
# same room but not from a room where the view is 0.6.
return None
# Assign blame to whichever value is smallest.
return sight_culprit(self.item[tag].prominence, view, lit)
def can_see(self, actor, tag):
'Is the item identified by "tag" visible to "actor"?'
return self.prevents_sight(actor, tag) is None
def reset(self):
'Revert the World and Concepts to their initial states.'
self.undo(1)
for actor in self.concept:
self.concept[actor].roll_back_to(1)
def set_concepts(self, actors):
"Set initial information in all Actors' Concepts."
for actor in self.item:
if self.has('actor', actor) and not actor == '@cosmos':
known_items = []
for i in self.item:
if self.can_see(actor, i):
known_items.append(copy.deepcopy(self.item[i]))
self.concept[actor] = Concept(known_items, {})
for (actor, items, actions) in actors:
self.concept[actor] = Concept(items, actions)
cosmos_items = []
for i in self.item:
if not i == '@cosmos':
cosmos_items.append(copy.deepcopy(self.item[i]))
cosmos_acts = copy.deepcopy(self.act)
self.concept['@cosmos'] = Concept(cosmos_items, cosmos_acts)
for actor in self.concept.keys():
self.concept[actor].concept_of = actor
def transfer(self, item, actor, time):
"Place an appropriate version of an Item in the Actor's Concept."
concept = self.concept[actor]
# If a Room, first add this Room as a child of @cosmos
if item.room and str(item) not in self.concept[actor].item:
new_cosmos = copy.deepcopy(concept.item['@cosmos'])
new_cosmos.add_child('in', str(item))
concept.update_item(new_cosmos, time)
# Now, the basic transfer applicable to all Items
if (str(item) not in concept.item or
not concept.item[str(item)] == item):
seen_item = copy.deepcopy(item)
concept.update_item(seen_item, time)
for (_, child) in item.children:
if self.can_see(actor, child):
self.transfer(self.item[child], actor, time)
# If a Room, add SharedThings & Doors to the Actor's Concept.
if item.room:
for shared_tag in self.item[str(item)].shared:
self.transfer(self.item[shared_tag], actor, time)
for door_tag in self.doors(str(item)):
self.transfer(self.item[door_tag], actor, time)
def transfer_out(self, item, actor, time):
"Remove the Item from the Actor's Concept."
concept = self.concept[actor]
if str(item) in concept.item:
missing_item = copy.deepcopy(concept.item[str(item)])
missing_item.link = 'of'
missing_item.parent = '@cosmos'
concept.update_item(missing_item, time)
def undo(self, action_id):
'Revert the World back to the start time of the specified Action.'
new_ids = []
for i in self.act:
new_ids.append((i, self.act[i].start))
ids_times = sorted(new_ids, key=operator.itemgetter(1))
target_time = self.act[action_id].start
while len(ids_times) > 0 and ids_times[-1][1] >= target_time:
(last_id, _) = ids_times.pop()
last_action = self.act.pop(last_id)
last_action.undo(self)
self.back_up_clock(target_time)