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