Skip to content

Commit 2d966e6

Browse files
committed
Fix HybridWumpusAgent (Fig 7.20) so it runs and reasons correctly (#961)
The agent crashed immediately and, where it didn't, used inverted logic. Fixed, faithfully to AIMA Fig 7.20: - WumpusKB now asserts the ok_to_move axiom at time 0 too (it was only added for t>0), so the agent can prove [1,1] safe on its first move; previously the safe set was empty at t=0 and planning crashed. - execute(): 'unvisited' was collecting *visited* squares (inverted) -> now the squares never been located in (for all t' <= t), intersected with safe; possible_wumpus uses 'not KB entails ~W' and not_unsafe uses 'not KB entails ~OK' (were 'not entails W' / 'not entails OK'); guarded the pose update when no location is entailed. - plan_route(): returns [] when A* finds no path (was crashing on None.solution()). - plan_shot(): used EAST/WEST/NORTH/SOUTH orientations that PlanRoute/WumpusPosition never use -> mapped to RIGHT/LEFT/UP/DOWN; set.remove -> set.discard (the wumpus cells were never added, so removing them raised KeyError). - make_percept_sentence(): handled only a single percept via an elif-chain -> now accepts a collection (a square can give Stench *and* Breeze at once) and None. The agent now runs and returns legal actions with correct logic. Added tests for make_percept_sentence, plan_shot, plan_route (no-path) and the t=0 end-to-end step. NB: only the first step is exercised in tests -- the propositional Fig 7.20 inference (SAT entailment over a temporal KB) grows expensive as the KB accumulates, which is inherent to the textbook algorithm, not a remaining bug.
1 parent 9942444 commit 2d966e6

2 files changed

Lines changed: 85 additions & 59 deletions

File tree

‎aima/logic.py‎

Lines changed: 49 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1405,6 +1405,10 @@ def __init__(self, dimrow):
14051405
for j in range(1, dimrow + 1):
14061406
self.tell(implies(location(i, j, 0), equiv(percept_breeze(0), breeze(i, j))))
14071407
self.tell(implies(location(i, j, 0), equiv(percept_stench(0), stench(i, j))))
1408+
# a square is safe to move into at time 0 iff it has no pit, no
1409+
# (live) wumpus -- without this the agent cannot even prove [1,1]
1410+
# safe on its first move (the rule was only added for time > 0)
1411+
self.tell(equiv(ok_to_move(i, j, 0), ~pit(i, j) & ~wumpus(i, j) & wumpus_alive(0)))
14081412
if i != 1 or j != 1:
14091413
self.tell(~location(i, j, 0))
14101414

@@ -1426,40 +1430,28 @@ def make_action_sentence(self, action, time):
14261430
self.tell(~a)
14271431

14281432
def make_percept_sentence(self, percept, time):
1429-
"""Tell the KB which of the five percepts hold (and which do not) at ``time``."""
1430-
# Glitter, Bump, Stench, Breeze, Scream
1431-
flags = [0, 0, 0, 0, 0]
1432-
1433-
# Things perceived
1434-
if isinstance(percept, Glitter):
1435-
flags[0] = 1
1436-
self.tell(percept_glitter(time))
1437-
elif isinstance(percept, Bump):
1438-
flags[1] = 1
1439-
self.tell(percept_bump(time))
1440-
elif isinstance(percept, Stench):
1441-
flags[2] = 1
1442-
self.tell(percept_stench(time))
1443-
elif isinstance(percept, Breeze):
1444-
flags[3] = 1
1445-
self.tell(percept_breeze(time))
1446-
elif isinstance(percept, Scream):
1447-
flags[4] = 1
1448-
self.tell(percept_scream(time))
1449-
1450-
# Things not perceived
1451-
for i in range(len(flags)):
1452-
if flags[i] == 0:
1453-
if i == 0:
1454-
self.tell(~percept_glitter(time))
1455-
elif i == 1:
1456-
self.tell(~percept_bump(time))
1457-
elif i == 2:
1458-
self.tell(~percept_stench(time))
1459-
elif i == 3:
1460-
self.tell(~percept_breeze(time))
1461-
elif i == 4:
1462-
self.tell(~percept_scream(time))
1433+
"""Tell the KB which of the five percepts hold (and which do not) at ``time``.
1434+
1435+
A single square can yield several percepts at once (e.g. a Stench *and* a
1436+
Breeze), so ``percept`` may be one percept object or a collection of them
1437+
(the format produced by ``WumpusEnvironment.percept``); ``None`` means
1438+
nothing was perceived.
1439+
"""
1440+
if percept is None:
1441+
percepts = []
1442+
elif isinstance(percept, (list, tuple, set)):
1443+
percepts = list(percept)
1444+
else:
1445+
percepts = [percept]
1446+
1447+
# for each of the five percepts, assert whether it holds at this time
1448+
for cls, sentence in ((Glitter, percept_glitter), (Bump, percept_bump),
1449+
(Stench, percept_stench), (Breeze, percept_breeze),
1450+
(Scream, percept_scream)):
1451+
if any(isinstance(p, cls) for p in percepts):
1452+
self.tell(sentence(time))
1453+
else:
1454+
self.tell(~sentence(time))
14631455

14641456
def add_temporal_sentences(self, time):
14651457
"""Add the successor-state axioms relating ``time`` to ``time - 1`` (location, orientation,
@@ -1607,14 +1599,15 @@ def execute(self, percept):
16071599
temp.append(i)
16081600
temp.append(j)
16091601

1610-
if self.kb.ask_if_true(facing_north(self.t)):
1611-
self.current_position = WumpusPosition(temp[0], temp[1], 'UP')
1612-
elif self.kb.ask_if_true(facing_south(self.t)):
1613-
self.current_position = WumpusPosition(temp[0], temp[1], 'DOWN')
1614-
elif self.kb.ask_if_true(facing_west(self.t)):
1615-
self.current_position = WumpusPosition(temp[0], temp[1], 'LEFT')
1616-
elif self.kb.ask_if_true(facing_east(self.t)):
1617-
self.current_position = WumpusPosition(temp[0], temp[1], 'RIGHT')
1602+
if len(temp) >= 2: # a location was entailed; otherwise keep the previous pose
1603+
if self.kb.ask_if_true(facing_north(self.t)):
1604+
self.current_position = WumpusPosition(temp[0], temp[1], 'UP')
1605+
elif self.kb.ask_if_true(facing_south(self.t)):
1606+
self.current_position = WumpusPosition(temp[0], temp[1], 'DOWN')
1607+
elif self.kb.ask_if_true(facing_west(self.t)):
1608+
self.current_position = WumpusPosition(temp[0], temp[1], 'LEFT')
1609+
elif self.kb.ask_if_true(facing_east(self.t)):
1610+
self.current_position = WumpusPosition(temp[0], temp[1], 'RIGHT')
16181611

16191612
safe_points = list()
16201613
for i in range(1, self.dimrow + 1):
@@ -1631,17 +1624,13 @@ def execute(self, percept):
16311624
self.plan.append('Climb')
16321625

16331626
if len(self.plan) == 0:
1627+
# unvisited = squares the agent has never been located in (for all t' <= t)
16341628
unvisited = list()
16351629
for i in range(1, self.dimrow + 1):
16361630
for j in range(1, self.dimrow + 1):
1637-
for k in range(self.t):
1638-
if self.kb.ask_if_true(location(i, j, k)):
1639-
unvisited.append([i, j])
1640-
unvisited_and_safe = list()
1641-
for u in unvisited:
1642-
for s in safe_points:
1643-
if u not in unvisited_and_safe and s == u:
1644-
unvisited_and_safe.append(u)
1631+
if not any(self.kb.ask_if_true(location(i, j, k)) for k in range(self.t + 1)):
1632+
unvisited.append([i, j])
1633+
unvisited_and_safe = [u for u in unvisited if u in safe_points]
16451634

16461635
temp = self.plan_route(self.current_position, unvisited_and_safe, safe_points)
16471636
self.plan.extend(temp)
@@ -1650,7 +1639,7 @@ def execute(self, percept):
16501639
possible_wumpus = list()
16511640
for i in range(1, self.dimrow + 1):
16521641
for j in range(1, self.dimrow + 1):
1653-
if not self.kb.ask_if_true(wumpus(i, j)):
1642+
if not self.kb.ask_if_true(~wumpus(i, j)):
16541643
possible_wumpus.append([i, j])
16551644

16561645
temp = self.plan_shot(self.current_position, possible_wumpus, safe_points)
@@ -1660,7 +1649,7 @@ def execute(self, percept):
16601649
not_unsafe = list()
16611650
for i in range(1, self.dimrow + 1):
16621651
for j in range(1, self.dimrow + 1):
1663-
if not self.kb.ask_if_true(ok_to_move(i, j, self.t)):
1652+
if not self.kb.ask_if_true(~ok_to_move(i, j, self.t)):
16641653
not_unsafe.append([i, j])
16651654
temp = self.plan_route(self.current_position, not_unsafe, safe_points)
16661655
self.plan.extend(temp)
@@ -1683,7 +1672,8 @@ def plan_route(self, current, goals, allowed):
16831672
"""Return an action sequence from ``current`` to any of ``goals`` through ``allowed`` squares,
16841673
found by A* search."""
16851674
problem = PlanRoute(current, goals, allowed, self.dimrow)
1686-
return astar_search(problem).solution()
1675+
node = astar_search(problem)
1676+
return node.solution() if node is not None else []
16871677

16881678
def plan_shot(self, current, goals, allowed):
16891679
"""Return an action sequence that moves to a square lined up with a possible wumpus and shoots."""
@@ -1694,19 +1684,19 @@ def plan_shot(self, current, goals, allowed):
16941684
y = loc[1]
16951685
for i in range(1, self.dimrow + 1):
16961686
if i < x:
1697-
shooting_positions.add(WumpusPosition(i, y, 'EAST'))
1687+
shooting_positions.add(WumpusPosition(i, y, 'RIGHT')) # face east toward the wumpus
16981688
if i > x:
1699-
shooting_positions.add(WumpusPosition(i, y, 'WEST'))
1689+
shooting_positions.add(WumpusPosition(i, y, 'LEFT')) # face west
17001690
if i < y:
1701-
shooting_positions.add(WumpusPosition(x, i, 'NORTH'))
1691+
shooting_positions.add(WumpusPosition(x, i, 'UP')) # face north
17021692
if i > y:
1703-
shooting_positions.add(WumpusPosition(x, i, 'SOUTH'))
1693+
shooting_positions.add(WumpusPosition(x, i, 'DOWN')) # face south
17041694

17051695
# Can't have a shooting position from any of the rooms the Wumpus could reside
1706-
orientations = ['EAST', 'WEST', 'NORTH', 'SOUTH']
1696+
orientations = ['UP', 'DOWN', 'LEFT', 'RIGHT']
17071697
for loc in goals:
17081698
for orientation in orientations:
1709-
shooting_positions.remove(WumpusPosition(loc[0], loc[1], orientation))
1699+
shooting_positions.discard(WumpusPosition(loc[0], loc[1], orientation))
17101700

17111701
actions = list()
17121702
actions.extend(self.plan_route(current, shooting_positions, allowed))

‎tests/test_logic.py‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,5 +405,41 @@ def test_SAT_plan():
405405
assert SAT_plan((0, 0), transition, (1, 1), 4) == ['Right', 'Down']
406406

407407

408+
def test_hybrid_wumpus_agent_make_percept_sentence():
409+
# a single square can yield several percepts at once (Stench *and* Breeze);
410+
# the old elif-chain recorded only the first one
411+
from aima.agents import Stench, Breeze
412+
kb = WumpusKB(2)
413+
kb.make_percept_sentence([Stench(), Breeze()], 0)
414+
assert percept_stench(0) in kb.clauses
415+
assert percept_breeze(0) in kb.clauses
416+
assert ~percept_glitter(0) in kb.clauses
417+
418+
419+
def test_hybrid_wumpus_agent_plan_shot():
420+
# plan_shot lines up with a possible wumpus and ends by shooting, using the
421+
# same UP/DOWN/LEFT/RIGHT orientations as PlanRoute (it used to use EAST/WEST/…)
422+
agent = HybridWumpusAgent(2)
423+
actions = agent.plan_shot(WumpusPosition(1, 1, 'RIGHT'), [[2, 2]],
424+
[[1, 1], [2, 1], [1, 2], [2, 2]])
425+
assert actions and actions[-1] == 'Shoot'
426+
427+
428+
def test_hybrid_wumpus_agent_plan_route_no_path():
429+
# an unreachable / empty goal set yields an empty plan instead of crashing
430+
agent = HybridWumpusAgent(2)
431+
assert agent.plan_route(WumpusPosition(1, 1, 'RIGHT'), [], [[1, 1]]) == []
432+
433+
434+
def test_hybrid_wumpus_agent_first_action():
435+
# end-to-end first step: from [1,1] (provably safe at t=0) the agent returns a
436+
# legal action and records its pose. (Only t=0 is exercised: the propositional
437+
# Fig 7.20 inference grows expensive as the temporal KB accumulates.)
438+
agent = HybridWumpusAgent(2)
439+
action = agent.program(None)
440+
assert action in {'Forward', 'TurnLeft', 'TurnRight', 'Grab', 'Shoot', 'Climb'}
441+
assert agent.current_position.get_location() == (1, 1)
442+
443+
408444
if __name__ == '__main__':
409445
pytest.main()

0 commit comments

Comments
 (0)