Skip to content

Commit 4a6ca8d

Browse files
committed
Fix PlanRoute, WumpusPosition and WumpusKB inference; correct last-action axiom (#1249, partial #961)
Several independently-verifiable defects in the Hybrid Wumpus Agent stack: - PlanRoute (search.py) mutated the search state in place (corrupting A*'s tree), wrote the y coordinate as a list (set_location(x, [y])), and its goal_test/h assumed a single (x, y) goal while callers pass a list of cells. Rewrite result() to return a fresh state, and make goal_test/h accept a collection of goal cells or position objects (matching orientation too, for shooting positions). - WumpusPosition (logic.py) defined __eq__ but no __hash__, so it was unhashable and could not be used in A*'s explored set or plan_shot's set. Add __hash__. - WumpusKB.ask_if_true used pl_resolution (full resolution closure), which does not terminate on the wumpus clause set. Use SAT entailment (KB & ~query unsatisfiable); verified to match tt_entails and to answer in well under a second. - The 'rules about last action' axiom used a biconditional, wrongly forcing MoveForward whenever the agent is not turning; it must be an implication (the agent may instead grab/shoot/climb) (#1249). Add a PlanRoute route-planning test. The remaining HybridWumpusAgent work (execute() unvisited/orientation logic and WumpusEnvironment integration) is tracked in #961.
1 parent ad7fd82 commit 4a6ca8d

3 files changed

Lines changed: 69 additions & 41 deletions

File tree

logic.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1441,8 +1441,9 @@ def add_temporal_sentences(self, time):
14411441
s = equiv(facing_south(time), a | b | c)
14421442
self.tell(s)
14431443

1444-
# Rules about last action
1445-
self.tell(equiv(move_forward(t), ~turn_right(t) & ~turn_left(t)))
1444+
# Rules about last action: moving forward implies the agent did not turn
1445+
# (the converse does not hold -- the agent may instead grab, shoot or climb)
1446+
self.tell(implies(move_forward(t), ~turn_right(t) & ~turn_left(t)))
14461447

14471448
# Rule about the arrow
14481449
self.tell(equiv(have_arrow(time), have_arrow(t) & ~shoot(t)))
@@ -1451,7 +1452,10 @@ def add_temporal_sentences(self, time):
14511452
self.tell(equiv(wumpus_alive(time), wumpus_alive(t) & ~percept_scream(time)))
14521453

14531454
def ask_if_true(self, query):
1454-
return pl_resolution(self, query)
1455+
# the KB entails the query iff KB & ~query is unsatisfiable; using a SAT
1456+
# solver here instead of pl_resolution keeps inference tractable on the
1457+
# large wumpus clause set (full resolution closure does not terminate)
1458+
return not dpll_satisfiable(associate('&', self.clauses + conjuncts(to_cnf(~query))))
14551459

14561460

14571461
# ______________________________________________________________________________
@@ -1482,6 +1486,9 @@ def __eq__(self, other):
14821486
else:
14831487
return False
14841488

1489+
def __hash__(self):
1490+
return hash((self.X, self.Y, self.orientation))
1491+
14851492

14861493
# ______________________________________________________________________________
14871494

search.py

Lines changed: 32 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -779,67 +779,61 @@ def actions(self, state):
779779

780780
def result(self, state, action):
781781
""" Given state and action, return a new state that is the result of the action.
782-
Action is assumed to be a valid action in the state """
782+
Action is assumed to be a valid action in the state. A fresh state object is
783+
returned; the input state is never mutated, so the search tree stays consistent. """
783784
x, y = state.get_location()
784-
proposed_loc = list()
785+
orientation = state.get_orientation()
786+
proposed_loc = None
787+
new_orientation = orientation
785788

786789
# Move Forward
787790
if action == 'Forward':
788-
if state.get_orientation() == 'UP':
791+
if orientation == 'UP':
789792
proposed_loc = [x, y + 1]
790-
elif state.get_orientation() == 'DOWN':
793+
elif orientation == 'DOWN':
791794
proposed_loc = [x, y - 1]
792-
elif state.get_orientation() == 'LEFT':
795+
elif orientation == 'LEFT':
793796
proposed_loc = [x - 1, y]
794-
elif state.get_orientation() == 'RIGHT':
797+
elif orientation == 'RIGHT':
795798
proposed_loc = [x + 1, y]
796799
else:
797800
raise Exception('InvalidOrientation')
798801

799802
# Rotate counter-clockwise
800803
elif action == 'TurnLeft':
801-
if state.get_orientation() == 'UP':
802-
state.set_orientation('LEFT')
803-
elif state.get_orientation() == 'DOWN':
804-
state.set_orientation('RIGHT')
805-
elif state.get_orientation() == 'LEFT':
806-
state.set_orientation('DOWN')
807-
elif state.get_orientation() == 'RIGHT':
808-
state.set_orientation('UP')
809-
else:
810-
raise Exception('InvalidOrientation')
804+
new_orientation = {'UP': 'LEFT', 'DOWN': 'RIGHT', 'LEFT': 'DOWN', 'RIGHT': 'UP'}[orientation]
811805

812806
# Rotate clockwise
813807
elif action == 'TurnRight':
814-
if state.get_orientation() == 'UP':
815-
state.set_orientation('RIGHT')
816-
elif state.get_orientation() == 'DOWN':
817-
state.set_orientation('LEFT')
818-
elif state.get_orientation() == 'LEFT':
819-
state.set_orientation('UP')
820-
elif state.get_orientation() == 'RIGHT':
821-
state.set_orientation('DOWN')
822-
else:
823-
raise Exception('InvalidOrientation')
808+
new_orientation = {'UP': 'RIGHT', 'DOWN': 'LEFT', 'LEFT': 'UP', 'RIGHT': 'DOWN'}[orientation]
824809

825-
if proposed_loc in self.allowed:
826-
state.set_location(proposed_loc[0], [proposed_loc[1]])
810+
new_x, new_y = x, y
811+
if proposed_loc is not None and proposed_loc in self.allowed:
812+
new_x, new_y = proposed_loc[0], proposed_loc[1]
827813

828-
return state
814+
return type(state)(new_x, new_y, new_orientation)
829815

830816
def goal_test(self, state):
831-
""" Given a state, return True if state is a goal state or False, otherwise """
832-
833-
return state.get_location() == tuple(self.goal)
817+
""" Return True when the state matches any goal position. The goal is a
818+
collection of [x, y] cells or of position objects; for the latter the
819+
orientation must match too (e.g. a shooting position). """
820+
for goal in self.goal:
821+
if hasattr(goal, 'get_location'):
822+
if (state.get_location() == goal.get_location() and
823+
state.get_orientation() == goal.get_orientation()):
824+
return True
825+
elif list(state.get_location()) == list(goal):
826+
return True
827+
return False
834828

835829
def h(self, node):
836-
""" Return the heuristic value for a given state."""
837-
838-
# Manhattan Heuristic Function
830+
""" Manhattan distance from the node's location to the nearest goal cell. """
839831
x1, y1 = node.state.get_location()
840-
x2, y2 = self.goal
841-
842-
return abs(x2 - x1) + abs(y2 - y1)
832+
distances = []
833+
for goal in self.goal:
834+
x2, y2 = goal.get_location() if hasattr(goal, 'get_location') else (goal[0], goal[1])
835+
distances.append(abs(x2 - x1) + abs(y2 - y1))
836+
return min(distances) if distances else 0
843837

844838

845839
# ______________________________________________________________________________

tests/test_search.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import pytest
22
from search import *
3+
from logic import WumpusPosition
34

45
random.seed("aima-python")
56

@@ -443,5 +444,31 @@ def search(self, problem):
443444
(['E', 'P', 'R', 'D', 'O', 'A', 'G', 'S', 'T'], 123)
444445
"""
445446

447+
def test_plan_route():
448+
dim = 4
449+
allowed = [[i, j] for i in range(1, dim + 1) for j in range(1, dim + 1)]
450+
start = WumpusPosition(1, 1, 'UP')
451+
452+
# a route to a goal cell: the planned actions must actually reach it
453+
problem = PlanRoute(start, [[3, 3]], allowed, dim)
454+
state = start
455+
for action in astar_search(problem).solution():
456+
state = problem.result(state, action)
457+
assert list(state.get_location()) == [3, 3]
458+
# the A* search must not mutate the initial state
459+
assert start.get_location() == (1, 1) and start.get_orientation() == 'UP'
460+
461+
# a position goal also constrains the final orientation
462+
problem = PlanRoute(start, [WumpusPosition(2, 1, 'RIGHT')], allowed, dim)
463+
state = start
464+
for action in astar_search(problem).solution():
465+
state = problem.result(state, action)
466+
assert state.get_location() == (2, 1) and state.get_orientation() == 'RIGHT'
467+
468+
# forward into a cell that is not allowed (unsafe) leaves the agent in place
469+
problem = PlanRoute(start, [[2, 2]], [[1, 1]], dim)
470+
assert problem.result(WumpusPosition(1, 1, 'RIGHT'), 'Forward').get_location() == (1, 1)
471+
472+
446473
if __name__ == '__main__':
447474
pytest.main()

0 commit comments

Comments
 (0)