Skip to content

Commit 59714b3

Browse files
authored
Add PourProblem (water-pouring) search problem (#1333)
Canonical water-pouring problem as a Problem subclass + test. Supersedes #1029 by @geekypathak21.
1 parent 39bc660 commit 59714b3

2 files changed

Lines changed: 48 additions & 0 deletions

File tree

‎search.py‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -883,6 +883,46 @@ def and_search(states, problem, path):
883883
directions8.update({'NW': (-1, 1), 'NE': (1, 1), 'SE': (1, -1), 'SW': (-1, -1)})
884884

885885

886+
class PourProblem(Problem):
887+
"""Problem about pouring water between jugs to achieve some water level.
888+
Each state is a tuple of levels. In the initialization, provide a tuple of
889+
capacities, e.g. PourProblem(initial=(2, 4, 3), goals={7}, capacities=(8, 16, 32)),
890+
which means three jugs of capacity 8, 16, 32 currently filled with 2, 4, 3 units
891+
of water, respectively, and the goal is to get a level of 7 in any one of the jugs."""
892+
893+
def __init__(self, initial=None, goals=(), capacities=None):
894+
super().__init__(initial, goals)
895+
self.goals = goals
896+
self.capacities = capacities
897+
898+
def actions(self, state):
899+
"""The actions executable in this state: fill or dump any jug, or pour one into another."""
900+
jugs = range(len(state))
901+
return ([('Fill', i) for i in jugs if state[i] != self.capacities[i]] +
902+
[('Dump', i) for i in jugs if state[i] != 0] +
903+
[('Pour', i, j) for i in jugs for j in jugs if i != j])
904+
905+
def result(self, state, action):
906+
"""The state that results from executing this action in this state."""
907+
result = list(state)
908+
act, i, j = action[0], action[1], action[-1]
909+
if act == 'Fill': # fill jug i to its capacity
910+
result[i] = self.capacities[i]
911+
elif act == 'Dump': # empty jug i
912+
result[i] = 0
913+
elif act == 'Pour': # pour jug i into jug j
914+
a, b = state[i], state[j]
915+
result[i], result[j] = ((0, a + b) if (a + b <= self.capacities[j])
916+
else (a + b - self.capacities[j], self.capacities[j]))
917+
else:
918+
raise ValueError('unknown action', action)
919+
return tuple(result)
920+
921+
def goal_test(self, state):
922+
"""True if any of the jugs has a level equal to one of the goal levels."""
923+
return any(level in self.goals for level in state)
924+
925+
886926
class PeakFindingProblem(Problem):
887927
"""Problem of finding the highest peak in a limited grid"""
888928

‎tests/test_search.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ def test_traveling_salesman():
102102
assert tsp.value(solution) == pytest.approx(3 + 5 ** 0.5)
103103

104104

105+
def test_pour_problem():
106+
# the classic two-jug puzzle: with jugs of capacity 3 and 5, measure out 4
107+
problem = PourProblem(initial=(0, 0), goals={4}, capacities=(3, 5))
108+
solution = breadth_first_graph_search(problem)
109+
assert solution is not None
110+
assert any(level == 4 for level in solution.state)
111+
112+
105113
def test_find_blank_square():
106114
assert eight_puzzle.find_blank_square((0, 1, 2, 3, 4, 5, 6, 7, 8)) == 0
107115
assert eight_puzzle.find_blank_square((6, 3, 5, 1, 8, 4, 2, 0, 7)) == 7

0 commit comments

Comments
 (0)