Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aimacode
GitHub Repository: aimacode/aima-python
Path: blob/master/agents4e.py
615 views
1
"""
2
Implement Agents and Environments. (Chapters 1-2)
3
4
The class hierarchies are as follows:
5
6
Thing ## A physical object that can exist in an environment
7
Agent
8
Wumpus
9
Dirt
10
Wall
11
...
12
13
Environment ## An environment holds objects, runs simulations
14
XYEnvironment
15
VacuumEnvironment
16
WumpusEnvironment
17
18
An agent program is a callable instance, taking percepts and choosing actions
19
SimpleReflexAgentProgram
20
...
21
22
EnvGUI ## A window with a graphical representation of the Environment
23
24
EnvToolbar ## contains buttons for controlling EnvGUI
25
26
EnvCanvas ## Canvas to display the environment of an EnvGUI
27
"""
28
29
# TODO
30
# Implement grabbing correctly.
31
# When an object is grabbed, does it still have a location?
32
# What if it is released?
33
# What if the grabbed or the grabber is deleted?
34
# What if the grabber moves?
35
# Speed control in GUI does not have any effect -- fix it.
36
37
from utils4e import distance_squared, turn_heading
38
from statistics import mean
39
from ipythonblocks import BlockGrid
40
from IPython.display import HTML, display, clear_output
41
from time import sleep
42
43
import random
44
import copy
45
import collections
46
import numbers
47
48
49
# ______________________________________________________________________________
50
51
52
class Thing:
53
"""This represents any physical object that can appear in an Environment.
54
You subclass Thing to get the things you want. Each thing can have a
55
.__name__ slot (used for output only)."""
56
57
def __repr__(self):
58
return '<{}>'.format(getattr(self, '__name__', self.__class__.__name__))
59
60
def is_alive(self):
61
"""Things that are 'alive' should return true."""
62
return hasattr(self, 'alive') and self.alive
63
64
def show_state(self):
65
"""Display the agent's internal state. Subclasses should override."""
66
print("I don't know how to show_state.")
67
68
def display(self, canvas, x, y, width, height):
69
"""Display an image of this Thing on the canvas."""
70
# Do we need this?
71
pass
72
73
74
class Agent(Thing):
75
"""An Agent is a subclass of Thing with one required slot,
76
.program, which should hold a function that takes one argument, the
77
percept, and returns an action. (What counts as a percept or action
78
will depend on the specific environment in which the agent exists.)
79
Note that 'program' is a slot, not a method. If it were a method,
80
then the program could 'cheat' and look at aspects of the agent.
81
It's not supposed to do that: the program can only look at the
82
percepts. An agent program that needs a model of the world (and of
83
the agent itself) will have to build and maintain its own model.
84
There is an optional slot, .performance, which is a number giving
85
the performance measure of the agent in its environment."""
86
87
def __init__(self, program=None):
88
self.alive = True
89
self.bump = False
90
self.holding = []
91
self.performance = 0
92
if program is None or not isinstance(program, collections.abc.Callable):
93
print("Can't find a valid program for {}, falling back to default.".format(self.__class__.__name__))
94
95
def program(percept):
96
return eval(input('Percept={}; action? '.format(percept)))
97
98
self.program = program
99
100
def can_grab(self, thing):
101
"""Return True if this agent can grab this thing.
102
Override for appropriate subclasses of Agent and Thing."""
103
return False
104
105
106
def TraceAgent(agent):
107
"""Wrap the agent's program to print its input and output. This will let
108
you see what the agent is doing in the environment."""
109
old_program = agent.program
110
111
def new_program(percept):
112
action = old_program(percept)
113
print('{} perceives {} and does {}'.format(agent, percept, action))
114
return action
115
116
agent.program = new_program
117
return agent
118
119
120
# ______________________________________________________________________________
121
122
123
def TableDrivenAgentProgram(table):
124
"""
125
[Figure 2.7]
126
This agent selects an action based on the percept sequence.
127
It is practical only for tiny domains.
128
To customize it, provide as table a dictionary of all
129
{percept_sequence:action} pairs.
130
"""
131
percepts = []
132
133
def program(percept):
134
percepts.append(percept)
135
action = table.get(tuple(percepts))
136
return action
137
138
return program
139
140
141
def RandomAgentProgram(actions):
142
"""An agent that chooses an action at random, ignoring all percepts.
143
>>> list = ['Right', 'Left', 'Suck', 'NoOp']
144
>>> program = RandomAgentProgram(list)
145
>>> agent = Agent(program)
146
>>> environment = TrivialVacuumEnvironment()
147
>>> environment.add_thing(agent)
148
>>> environment.run()
149
>>> environment.status == {(1, 0): 'Clean' , (0, 0): 'Clean'}
150
True
151
"""
152
return lambda percept: random.choice(actions)
153
154
155
# ______________________________________________________________________________
156
157
158
def SimpleReflexAgentProgram(rules, interpret_input):
159
"""
160
[Figure 2.10]
161
This agent takes action based solely on the percept.
162
"""
163
164
def program(percept):
165
state = interpret_input(percept)
166
rule = rule_match(state, rules)
167
action = rule.action
168
return action
169
170
return program
171
172
173
def ModelBasedReflexAgentProgram(rules, update_state, transition_model, sensor_model):
174
"""
175
[Figure 2.12]
176
This agent takes action based on the percept and state.
177
"""
178
179
def program(percept):
180
program.state = update_state(program.state, program.action, percept, transition_model, sensor_model)
181
rule = rule_match(program.state, rules)
182
action = rule.action
183
return action
184
185
program.state = program.action = None
186
return program
187
188
189
def rule_match(state, rules):
190
"""Find the first rule that matches state."""
191
for rule in rules:
192
if rule.matches(state):
193
return rule
194
195
196
# ______________________________________________________________________________
197
198
199
loc_A, loc_B = (0, 0), (1, 0) # The two locations for the Vacuum world
200
201
202
def RandomVacuumAgent():
203
"""Randomly choose one of the actions from the vacuum environment.
204
>>> agent = RandomVacuumAgent()
205
>>> environment = TrivialVacuumEnvironment()
206
>>> environment.add_thing(agent)
207
>>> environment.run()
208
>>> environment.status == {(1,0):'Clean' , (0,0) : 'Clean'}
209
True
210
"""
211
return Agent(RandomAgentProgram(['Right', 'Left', 'Suck', 'NoOp']))
212
213
214
def TableDrivenVacuumAgent():
215
"""Tabular approach towards vacuum world as mentioned in [Figure 2.3]
216
>>> agent = TableDrivenVacuumAgent()
217
>>> environment = TrivialVacuumEnvironment()
218
>>> environment.add_thing(agent)
219
>>> environment.run()
220
>>> environment.status == {(1,0):'Clean' , (0,0) : 'Clean'}
221
True
222
"""
223
table = {((loc_A, 'Clean'),): 'Right',
224
((loc_A, 'Dirty'),): 'Suck',
225
((loc_B, 'Clean'),): 'Left',
226
((loc_B, 'Dirty'),): 'Suck',
227
((loc_A, 'Dirty'), (loc_A, 'Clean')): 'Right',
228
((loc_A, 'Clean'), (loc_B, 'Dirty')): 'Suck',
229
((loc_B, 'Clean'), (loc_A, 'Dirty')): 'Suck',
230
((loc_B, 'Dirty'), (loc_B, 'Clean')): 'Left',
231
((loc_A, 'Dirty'), (loc_A, 'Clean'), (loc_B, 'Dirty')): 'Suck',
232
((loc_B, 'Dirty'), (loc_B, 'Clean'), (loc_A, 'Dirty')): 'Suck'}
233
return Agent(TableDrivenAgentProgram(table))
234
235
236
def ReflexVacuumAgent():
237
"""
238
[Figure 2.8]
239
A reflex agent for the two-state vacuum environment.
240
>>> agent = ReflexVacuumAgent()
241
>>> environment = TrivialVacuumEnvironment()
242
>>> environment.add_thing(agent)
243
>>> environment.run()
244
>>> environment.status == {(1,0):'Clean' , (0,0) : 'Clean'}
245
True
246
"""
247
248
def program(percept):
249
location, status = percept
250
if status == 'Dirty':
251
return 'Suck'
252
elif location == loc_A:
253
return 'Right'
254
elif location == loc_B:
255
return 'Left'
256
257
return Agent(program)
258
259
260
def ModelBasedVacuumAgent():
261
"""An agent that keeps track of what locations are clean or dirty.
262
>>> agent = ModelBasedVacuumAgent()
263
>>> environment = TrivialVacuumEnvironment()
264
>>> environment.add_thing(agent)
265
>>> environment.run()
266
>>> environment.status == {(1,0):'Clean' , (0,0) : 'Clean'}
267
True
268
"""
269
model = {loc_A: None, loc_B: None}
270
271
def program(percept):
272
"""Same as ReflexVacuumAgent, except if everything is clean, do NoOp."""
273
location, status = percept
274
model[location] = status # Update the model here
275
if model[loc_A] == model[loc_B] == 'Clean':
276
return 'NoOp'
277
elif status == 'Dirty':
278
return 'Suck'
279
elif location == loc_A:
280
return 'Right'
281
elif location == loc_B:
282
return 'Left'
283
284
return Agent(program)
285
286
287
# ______________________________________________________________________________
288
289
290
class Environment:
291
"""Abstract class representing an Environment. 'Real' Environment classes
292
inherit from this. Your Environment will typically need to implement:
293
percept: Define the percept that an agent sees.
294
execute_action: Define the effects of executing an action.
295
Also update the agent.performance slot.
296
The environment keeps a list of .things and .agents (which is a subset
297
of .things). Each agent has a .performance slot, initialized to 0.
298
Each thing has a .location slot, even though some environments may not
299
need this."""
300
301
def __init__(self):
302
self.things = []
303
self.agents = []
304
305
def thing_classes(self):
306
return [] # List of classes that can go into environment
307
308
def percept(self, agent):
309
"""Return the percept that the agent sees at this point. (Implement this.)"""
310
raise NotImplementedError
311
312
def execute_action(self, agent, action):
313
"""Change the world to reflect this action. (Implement this.)"""
314
raise NotImplementedError
315
316
def default_location(self, thing):
317
"""Default location to place a new thing with unspecified location."""
318
return None
319
320
def exogenous_change(self):
321
"""If there is spontaneous change in the world, override this."""
322
pass
323
324
def is_done(self):
325
"""By default, we're done when we can't find a live agent."""
326
return not any(agent.is_alive() for agent in self.agents)
327
328
def step(self):
329
"""Run the environment for one time step. If the
330
actions and exogenous changes are independent, this method will
331
do. If there are interactions between them, you'll need to
332
override this method."""
333
if not self.is_done():
334
actions = []
335
for agent in self.agents:
336
if agent.alive:
337
actions.append(agent.program(self.percept(agent)))
338
else:
339
actions.append("")
340
for (agent, action) in zip(self.agents, actions):
341
self.execute_action(agent, action)
342
self.exogenous_change()
343
344
def run(self, steps=1000):
345
"""Run the Environment for given number of time steps."""
346
for step in range(steps):
347
if self.is_done():
348
return
349
self.step()
350
351
def list_things_at(self, location, tclass=Thing):
352
"""Return all things exactly at a given location."""
353
if isinstance(location, numbers.Number):
354
return [thing for thing in self.things
355
if thing.location == location and isinstance(thing, tclass)]
356
return [thing for thing in self.things
357
if all(x == y for x, y in zip(thing.location, location)) and isinstance(thing, tclass)]
358
359
def some_things_at(self, location, tclass=Thing):
360
"""Return true if at least one of the things at location
361
is an instance of class tclass (or a subclass)."""
362
return self.list_things_at(location, tclass) != []
363
364
def add_thing(self, thing, location=None):
365
"""Add a thing to the environment, setting its location. For
366
convenience, if thing is an agent program we make a new agent
367
for it. (Shouldn't need to override this.)"""
368
if not isinstance(thing, Thing):
369
thing = Agent(thing)
370
if thing in self.things:
371
print("Can't add the same thing twice")
372
else:
373
thing.location = location if location is not None else self.default_location(thing)
374
self.things.append(thing)
375
if isinstance(thing, Agent):
376
thing.performance = 0
377
self.agents.append(thing)
378
379
def delete_thing(self, thing):
380
"""Remove a thing from the environment."""
381
try:
382
self.things.remove(thing)
383
except ValueError as e:
384
print(e)
385
print(" in Environment delete_thing")
386
print(" Thing to be removed: {} at {}".format(thing, thing.location))
387
print(" from list: {}".format([(thing, thing.location) for thing in self.things]))
388
if thing in self.agents:
389
self.agents.remove(thing)
390
391
392
class Direction:
393
"""A direction class for agents that want to move in a 2D plane
394
Usage:
395
d = Direction("down")
396
To change directions:
397
d = d + "right" or d = d + Direction.R #Both do the same thing
398
Note that the argument to __add__ must be a string and not a Direction object.
399
Also, it (the argument) can only be right or left."""
400
401
R = "right"
402
L = "left"
403
U = "up"
404
D = "down"
405
406
def __init__(self, direction):
407
self.direction = direction
408
409
def __add__(self, heading):
410
"""
411
>>> d = Direction('right')
412
>>> l1 = d.__add__(Direction.L)
413
>>> l2 = d.__add__(Direction.R)
414
>>> l1.direction
415
'up'
416
>>> l2.direction
417
'down'
418
>>> d = Direction('down')
419
>>> l1 = d.__add__('right')
420
>>> l2 = d.__add__('left')
421
>>> l1.direction == Direction.L
422
True
423
>>> l2.direction == Direction.R
424
True
425
"""
426
if self.direction == self.R:
427
return {
428
self.R: Direction(self.D),
429
self.L: Direction(self.U),
430
}.get(heading, None)
431
elif self.direction == self.L:
432
return {
433
self.R: Direction(self.U),
434
self.L: Direction(self.D),
435
}.get(heading, None)
436
elif self.direction == self.U:
437
return {
438
self.R: Direction(self.R),
439
self.L: Direction(self.L),
440
}.get(heading, None)
441
elif self.direction == self.D:
442
return {
443
self.R: Direction(self.L),
444
self.L: Direction(self.R),
445
}.get(heading, None)
446
447
def move_forward(self, from_location):
448
"""
449
>>> d = Direction('up')
450
>>> l1 = d.move_forward((0, 0))
451
>>> l1
452
(0, -1)
453
>>> d = Direction(Direction.R)
454
>>> l1 = d.move_forward((0, 0))
455
>>> l1
456
(1, 0)
457
"""
458
# get the iterable class to return
459
iclass = from_location.__class__
460
x, y = from_location
461
if self.direction == self.R:
462
return iclass((x + 1, y))
463
elif self.direction == self.L:
464
return iclass((x - 1, y))
465
elif self.direction == self.U:
466
return iclass((x, y - 1))
467
elif self.direction == self.D:
468
return iclass((x, y + 1))
469
470
471
class XYEnvironment(Environment):
472
"""This class is for environments on a 2D plane, with locations
473
labelled by (x, y) points, either discrete or continuous.
474
475
Agents perceive things within a radius. Each agent in the
476
environment has a .location slot which should be a location such
477
as (0, 1), and a .holding slot, which should be a list of things
478
that are held."""
479
480
def __init__(self, width=10, height=10):
481
super().__init__()
482
483
self.width = width
484
self.height = height
485
self.observers = []
486
# Sets iteration start and end (no walls).
487
self.x_start, self.y_start = (0, 0)
488
self.x_end, self.y_end = (self.width, self.height)
489
490
perceptible_distance = 1
491
492
def things_near(self, location, radius=None):
493
"""Return all things within radius of location."""
494
if radius is None:
495
radius = self.perceptible_distance
496
radius2 = radius * radius
497
return [(thing, radius2 - distance_squared(location, thing.location))
498
for thing in self.things if distance_squared(
499
location, thing.location) <= radius2]
500
501
def percept(self, agent):
502
"""By default, agent perceives things within a default radius."""
503
return self.things_near(agent.location)
504
505
def execute_action(self, agent, action):
506
agent.bump = False
507
if action == 'TurnRight':
508
agent.direction += Direction.R
509
elif action == 'TurnLeft':
510
agent.direction += Direction.L
511
elif action == 'Forward':
512
agent.bump = self.move_to(agent, agent.direction.move_forward(agent.location))
513
# elif action == 'Grab':
514
# things = [thing for thing in self.list_things_at(agent.location)
515
# if agent.can_grab(thing)]
516
# if things:
517
# agent.holding.append(things[0])
518
elif action == 'Release':
519
if agent.holding:
520
agent.holding.pop()
521
522
def default_location(self, thing):
523
location = self.random_location_inbounds()
524
while self.some_things_at(location, Obstacle):
525
# we will find a random location with no obstacles
526
location = self.random_location_inbounds()
527
return location
528
529
def move_to(self, thing, destination):
530
"""Move a thing to a new location. Returns True on success or False if there is an Obstacle.
531
If thing is holding anything, they move with him."""
532
thing.bump = self.some_things_at(destination, Obstacle)
533
if not thing.bump:
534
thing.location = destination
535
for o in self.observers:
536
o.thing_moved(thing)
537
for t in thing.holding:
538
self.delete_thing(t)
539
self.add_thing(t, destination)
540
t.location = destination
541
return thing.bump
542
543
def add_thing(self, thing, location=None, exclude_duplicate_class_items=False):
544
"""Add things to the world. If (exclude_duplicate_class_items) then the item won't be
545
added if the location has at least one item of the same class."""
546
if location is None:
547
super().add_thing(thing)
548
elif self.is_inbounds(location):
549
if (exclude_duplicate_class_items and
550
any(isinstance(t, thing.__class__) for t in self.list_things_at(location))):
551
return
552
super().add_thing(thing, location)
553
554
def is_inbounds(self, location):
555
"""Checks to make sure that the location is inbounds (within walls if we have walls)"""
556
x, y = location
557
return not (x < self.x_start or x > self.x_end or y < self.y_start or y > self.y_end)
558
559
def random_location_inbounds(self, exclude=None):
560
"""Returns a random location that is inbounds (within walls if we have walls)"""
561
location = (random.randint(self.x_start, self.x_end),
562
random.randint(self.y_start, self.y_end))
563
if exclude is not None:
564
while location == exclude:
565
location = (random.randint(self.x_start, self.x_end),
566
random.randint(self.y_start, self.y_end))
567
return location
568
569
def delete_thing(self, thing):
570
"""Deletes thing, and everything it is holding (if thing is an agent)"""
571
if isinstance(thing, Agent):
572
for obj in thing.holding:
573
super().delete_thing(obj)
574
for obs in self.observers:
575
obs.thing_deleted(obj)
576
577
super().delete_thing(thing)
578
for obs in self.observers:
579
obs.thing_deleted(thing)
580
581
def add_walls(self):
582
"""Put walls around the entire perimeter of the grid."""
583
for x in range(self.width):
584
self.add_thing(Wall(), (x, 0))
585
self.add_thing(Wall(), (x, self.height - 1))
586
for y in range(1, self.height - 1):
587
self.add_thing(Wall(), (0, y))
588
self.add_thing(Wall(), (self.width - 1, y))
589
590
# Updates iteration start and end (with walls).
591
self.x_start, self.y_start = (1, 1)
592
self.x_end, self.y_end = (self.width - 1, self.height - 1)
593
594
def add_observer(self, observer):
595
"""Adds an observer to the list of observers.
596
An observer is typically an EnvGUI.
597
598
Each observer is notified of changes in move_to and add_thing,
599
by calling the observer's methods thing_moved(thing)
600
and thing_added(thing, loc)."""
601
self.observers.append(observer)
602
603
def turn_heading(self, heading, inc):
604
"""Return the heading to the left (inc=+1) or right (inc=-1) of heading."""
605
return turn_heading(heading, inc)
606
607
608
class Obstacle(Thing):
609
"""Something that can cause a bump, preventing an agent from
610
moving into the same square it's in."""
611
pass
612
613
614
class Wall(Obstacle):
615
pass
616
617
618
# ______________________________________________________________________________
619
620
621
class GraphicEnvironment(XYEnvironment):
622
def __init__(self, width=10, height=10, boundary=True, color={}, display=False):
623
"""Define all the usual XYEnvironment characteristics,
624
but initialise a BlockGrid for GUI too."""
625
super().__init__(width, height)
626
self.grid = BlockGrid(width, height, fill=(200, 200, 200))
627
if display:
628
self.grid.show()
629
self.visible = True
630
else:
631
self.visible = False
632
self.bounded = boundary
633
self.colors = color
634
635
def get_world(self):
636
"""Returns all the items in the world in a format
637
understandable by the ipythonblocks BlockGrid."""
638
result = []
639
x_start, y_start = (0, 0)
640
x_end, y_end = self.width, self.height
641
for x in range(x_start, x_end):
642
row = []
643
for y in range(y_start, y_end):
644
row.append(self.list_things_at((x, y)))
645
result.append(row)
646
return result
647
648
"""
649
def run(self, steps=1000, delay=1):
650
"" "Run the Environment for given number of time steps,
651
but update the GUI too." ""
652
for step in range(steps):
653
sleep(delay)
654
if self.visible:
655
self.reveal()
656
if self.is_done():
657
if self.visible:
658
self.reveal()
659
return
660
self.step()
661
if self.visible:
662
self.reveal()
663
"""
664
665
def run(self, steps=1000, delay=1):
666
"""Run the Environment for given number of time steps,
667
but update the GUI too."""
668
for step in range(steps):
669
self.update(delay)
670
if self.is_done():
671
break
672
self.step()
673
self.update(delay)
674
675
def update(self, delay=1):
676
sleep(delay)
677
self.reveal()
678
679
def reveal(self):
680
"""Display the BlockGrid for this world - the last thing to be added
681
at a location defines the location color."""
682
self.draw_world()
683
# wait for the world to update and
684
# apply changes to the same grid instead
685
# of making a new one.
686
clear_output(1)
687
self.grid.show()
688
self.visible = True
689
690
def draw_world(self):
691
self.grid[:] = (200, 200, 200)
692
world = self.get_world()
693
for x in range(0, len(world)):
694
for y in range(0, len(world[x])):
695
if len(world[x][y]):
696
self.grid[y, x] = self.colors[world[x][y][-1].__class__.__name__]
697
698
def conceal(self):
699
"""Hide the BlockGrid for this world"""
700
self.visible = False
701
display(HTML(''))
702
703
704
# ______________________________________________________________________________
705
# Continuous environment
706
707
class ContinuousWorld(Environment):
708
"""Model for Continuous World"""
709
710
def __init__(self, width=10, height=10):
711
super().__init__()
712
self.width = width
713
self.height = height
714
715
def add_obstacle(self, coordinates):
716
self.things.append(PolygonObstacle(coordinates))
717
718
719
class PolygonObstacle(Obstacle):
720
721
def __init__(self, coordinates):
722
"""Coordinates is a list of tuples."""
723
super().__init__()
724
self.coordinates = coordinates
725
726
727
# ______________________________________________________________________________
728
# Vacuum environment
729
730
731
class Dirt(Thing):
732
pass
733
734
735
class VacuumEnvironment(XYEnvironment):
736
"""The environment of [Ex. 2.12]. Agent perceives dirty or clean,
737
and bump (into obstacle) or not; 2D discrete world of unknown size;
738
performance measure is 100 for each dirt cleaned, and -1 for
739
each turn taken."""
740
741
def __init__(self, width=10, height=10):
742
super().__init__(width, height)
743
self.add_walls()
744
745
def thing_classes(self):
746
return [Wall, Dirt, ReflexVacuumAgent, RandomVacuumAgent,
747
TableDrivenVacuumAgent, ModelBasedVacuumAgent]
748
749
def percept(self, agent):
750
"""The percept is a tuple of ('Dirty' or 'Clean', 'Bump' or 'None').
751
Unlike the TrivialVacuumEnvironment, location is NOT perceived."""
752
status = ('Dirty' if self.some_things_at(
753
agent.location, Dirt) else 'Clean')
754
bump = ('Bump' if agent.bump else 'None')
755
return status, bump
756
757
def execute_action(self, agent, action):
758
agent.bump = False
759
if action == 'Suck':
760
dirt_list = self.list_things_at(agent.location, Dirt)
761
if dirt_list != []:
762
dirt = dirt_list[0]
763
agent.performance += 100
764
self.delete_thing(dirt)
765
else:
766
super().execute_action(agent, action)
767
768
if action != 'NoOp':
769
agent.performance -= 1
770
771
772
class TrivialVacuumEnvironment(Environment):
773
"""This environment has two locations, A and B. Each can be Dirty
774
or Clean. The agent perceives its location and the location's
775
status. This serves as an example of how to implement a simple
776
Environment."""
777
778
def __init__(self):
779
super().__init__()
780
self.status = {loc_A: random.choice(['Clean', 'Dirty']),
781
loc_B: random.choice(['Clean', 'Dirty'])}
782
783
def thing_classes(self):
784
return [Wall, Dirt, ReflexVacuumAgent, RandomVacuumAgent, TableDrivenVacuumAgent, ModelBasedVacuumAgent]
785
786
def percept(self, agent):
787
"""Returns the agent's location, and the location status (Dirty/Clean)."""
788
return agent.location, self.status[agent.location]
789
790
def execute_action(self, agent, action):
791
"""Change agent's location and/or location's status; track performance.
792
Score 10 for each dirt cleaned; -1 for each move."""
793
if action == 'Right':
794
agent.location = loc_B
795
agent.performance -= 1
796
elif action == 'Left':
797
agent.location = loc_A
798
agent.performance -= 1
799
elif action == 'Suck':
800
if self.status[agent.location] == 'Dirty':
801
agent.performance += 10
802
self.status[agent.location] = 'Clean'
803
804
def default_location(self, thing):
805
"""Agents start in either location at random."""
806
return random.choice([loc_A, loc_B])
807
808
809
# ______________________________________________________________________________
810
# The Wumpus World
811
812
813
class Gold(Thing):
814
815
def __eq__(self, rhs):
816
"""All Gold are equal"""
817
return rhs.__class__ == Gold
818
819
pass
820
821
822
class Bump(Thing):
823
pass
824
825
826
class Glitter(Thing):
827
pass
828
829
830
class Pit(Thing):
831
pass
832
833
834
class Breeze(Thing):
835
pass
836
837
838
class Arrow(Thing):
839
pass
840
841
842
class Scream(Thing):
843
pass
844
845
846
class Wumpus(Agent):
847
screamed = False
848
pass
849
850
851
class Stench(Thing):
852
pass
853
854
855
class Explorer(Agent):
856
holding = []
857
has_arrow = True
858
killed_by = ""
859
direction = Direction("right")
860
861
def can_grab(self, thing):
862
"""Explorer can only grab gold"""
863
return thing.__class__ == Gold
864
865
866
class WumpusEnvironment(XYEnvironment):
867
pit_probability = 0.2 # Probability to spawn a pit in a location. (From Chapter 7.2)
868
869
# Room should be 4x4 grid of rooms. The extra 2 for walls
870
871
def __init__(self, agent_program, width=6, height=6):
872
super().__init__(width, height)
873
self.init_world(agent_program)
874
875
def init_world(self, program):
876
"""Spawn items in the world based on probabilities from the book"""
877
878
"WALLS"
879
self.add_walls()
880
881
"PITS"
882
for x in range(self.x_start, self.x_end):
883
for y in range(self.y_start, self.y_end):
884
if random.random() < self.pit_probability:
885
self.add_thing(Pit(), (x, y), True)
886
self.add_thing(Breeze(), (x - 1, y), True)
887
self.add_thing(Breeze(), (x, y - 1), True)
888
self.add_thing(Breeze(), (x + 1, y), True)
889
self.add_thing(Breeze(), (x, y + 1), True)
890
891
"WUMPUS"
892
w_x, w_y = self.random_location_inbounds(exclude=(1, 1))
893
self.add_thing(Wumpus(lambda x: ""), (w_x, w_y), True)
894
self.add_thing(Stench(), (w_x - 1, w_y), True)
895
self.add_thing(Stench(), (w_x + 1, w_y), True)
896
self.add_thing(Stench(), (w_x, w_y - 1), True)
897
self.add_thing(Stench(), (w_x, w_y + 1), True)
898
899
"GOLD"
900
self.add_thing(Gold(), self.random_location_inbounds(exclude=(1, 1)), True)
901
902
"AGENT"
903
self.add_thing(Explorer(program), (1, 1), True)
904
905
def get_world(self, show_walls=True):
906
"""Return the items in the world"""
907
result = []
908
x_start, y_start = (0, 0) if show_walls else (1, 1)
909
910
if show_walls:
911
x_end, y_end = self.width, self.height
912
else:
913
x_end, y_end = self.width - 1, self.height - 1
914
915
for x in range(x_start, x_end):
916
row = []
917
for y in range(y_start, y_end):
918
row.append(self.list_things_at((x, y)))
919
result.append(row)
920
return result
921
922
def percepts_from(self, agent, location, tclass=Thing):
923
"""Return percepts from a given location,
924
and replaces some items with percepts from chapter 7."""
925
thing_percepts = {
926
Gold: Glitter(),
927
Wall: Bump(),
928
Wumpus: Stench(),
929
Pit: Breeze()}
930
931
"""Agents don't need to get their percepts"""
932
thing_percepts[agent.__class__] = None
933
934
"""Gold only glitters in its cell"""
935
if location != agent.location:
936
thing_percepts[Gold] = None
937
938
result = [thing_percepts.get(thing.__class__, thing) for thing in self.things
939
if thing.location == location and isinstance(thing, tclass)]
940
return result if len(result) else [None]
941
942
def percept(self, agent):
943
"""Return things in adjacent (not diagonal) cells of the agent.
944
Result format: [Left, Right, Up, Down, Center / Current location]"""
945
x, y = agent.location
946
result = []
947
result.append(self.percepts_from(agent, (x - 1, y)))
948
result.append(self.percepts_from(agent, (x + 1, y)))
949
result.append(self.percepts_from(agent, (x, y - 1)))
950
result.append(self.percepts_from(agent, (x, y + 1)))
951
result.append(self.percepts_from(agent, (x, y)))
952
953
"""The wumpus gives out a loud scream once it's killed."""
954
wumpus = [thing for thing in self.things if isinstance(thing, Wumpus)]
955
if len(wumpus) and not wumpus[0].alive and not wumpus[0].screamed:
956
result[-1].append(Scream())
957
wumpus[0].screamed = True
958
959
return result
960
961
def execute_action(self, agent, action):
962
"""Modify the state of the environment based on the agent's actions.
963
Performance score taken directly out of the book."""
964
965
if isinstance(agent, Explorer) and self.in_danger(agent):
966
return
967
968
agent.bump = False
969
if action == 'TurnRight':
970
agent.direction += Direction.R
971
agent.performance -= 1
972
elif action == 'TurnLeft':
973
agent.direction += Direction.L
974
agent.performance -= 1
975
elif action == 'Forward':
976
agent.bump = self.move_to(agent, agent.direction.move_forward(agent.location))
977
agent.performance -= 1
978
elif action == 'Grab':
979
things = [thing for thing in self.list_things_at(agent.location)
980
if agent.can_grab(thing)]
981
if len(things):
982
print("Grabbing", things[0].__class__.__name__)
983
if len(things):
984
agent.holding.append(things[0])
985
agent.performance -= 1
986
elif action == 'Climb':
987
if agent.location == (1, 1): # Agent can only climb out of (1,1)
988
agent.performance += 1000 if Gold() in agent.holding else 0
989
self.delete_thing(agent)
990
elif action == 'Shoot':
991
"""The arrow travels straight down the path the agent is facing"""
992
if agent.has_arrow:
993
arrow_travel = agent.direction.move_forward(agent.location)
994
while self.is_inbounds(arrow_travel):
995
wumpus = [thing for thing in self.list_things_at(arrow_travel)
996
if isinstance(thing, Wumpus)]
997
if len(wumpus):
998
wumpus[0].alive = False
999
break
1000
arrow_travel = agent.direction.move_forward(agent.location)
1001
agent.has_arrow = False
1002
1003
def in_danger(self, agent):
1004
"""Check if Explorer is in danger (Pit or Wumpus), if he is, kill him"""
1005
for thing in self.list_things_at(agent.location):
1006
if isinstance(thing, Pit) or (isinstance(thing, Wumpus) and thing.alive):
1007
agent.alive = False
1008
agent.performance -= 1000
1009
agent.killed_by = thing.__class__.__name__
1010
return True
1011
return False
1012
1013
def is_done(self):
1014
"""The game is over when the Explorer is killed
1015
or if he climbs out of the cave only at (1,1)."""
1016
explorer = [agent for agent in self.agents if isinstance(agent, Explorer)]
1017
if len(explorer):
1018
if explorer[0].alive:
1019
return False
1020
else:
1021
print("Death by {} [-1000].".format(explorer[0].killed_by))
1022
else:
1023
print("Explorer climbed out {}."
1024
.format("with Gold [+1000]!" if Gold() not in self.things else "without Gold [+0]"))
1025
return True
1026
1027
# TODO: Arrow needs to be implemented
1028
1029
1030
# ______________________________________________________________________________
1031
1032
1033
def compare_agents(EnvFactory, AgentFactories, n=10, steps=1000):
1034
"""See how well each of several agents do in n instances of an environment.
1035
Pass in a factory (constructor) for environments, and several for agents.
1036
Create n instances of the environment, and run each agent in copies of
1037
each one for steps. Return a list of (agent, average-score) tuples.
1038
>>> environment = TrivialVacuumEnvironment
1039
>>> agents = [ModelBasedVacuumAgent, ReflexVacuumAgent]
1040
>>> result = compare_agents(environment, agents)
1041
>>> performance_ModelBasedVacuumAgent = result[0][1]
1042
>>> performance_ReflexVacuumAgent = result[1][1]
1043
>>> performance_ReflexVacuumAgent <= performance_ModelBasedVacuumAgent
1044
True
1045
"""
1046
envs = [EnvFactory() for i in range(n)]
1047
return [(A, test_agent(A, steps, copy.deepcopy(envs)))
1048
for A in AgentFactories]
1049
1050
1051
def test_agent(AgentFactory, steps, envs):
1052
"""Return the mean score of running an agent in each of the envs, for steps
1053
>>> def constant_prog(percept):
1054
... return percept
1055
...
1056
>>> agent = Agent(constant_prog)
1057
>>> result = agent.program(5)
1058
>>> result == 5
1059
True
1060
"""
1061
1062
def score(env):
1063
agent = AgentFactory()
1064
env.add_thing(agent)
1065
env.run(steps)
1066
return agent.performance
1067
1068
return mean(map(score, envs))
1069
1070
1071
# _________________________________________________________________________
1072
1073
1074
__doc__ += """
1075
>>> a = ReflexVacuumAgent()
1076
>>> a.program((loc_A, 'Clean'))
1077
'Right'
1078
>>> a.program((loc_B, 'Clean'))
1079
'Left'
1080
>>> a.program((loc_A, 'Dirty'))
1081
'Suck'
1082
>>> a.program((loc_A, 'Dirty'))
1083
'Suck'
1084
1085
>>> e = TrivialVacuumEnvironment()
1086
>>> e.add_thing(ModelBasedVacuumAgent())
1087
>>> e.run(5)
1088
1089
"""
1090
1091