co2114 2026.1.0__tar.gz → 2026.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {co2114-2026.1.0 → co2114-2026.1.2}/PKG-INFO +1 -1
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114/agent/environment.py +1 -1
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114/constraints/csp/__init__.py +4 -0
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114/optimisation/planning.py +125 -41
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114/optimisation/things.py +9 -0
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114/search/graph.py +6 -2
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114/search/maze.py +166 -38
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114/search/things.py +2 -2
- co2114-2026.1.2/co2114/search/util.py +49 -0
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114.egg-info/PKG-INFO +1 -1
- {co2114-2026.1.0 → co2114-2026.1.2}/setup.py +1 -1
- co2114-2026.1.0/co2114/search/util.py +0 -27
- {co2114-2026.1.0 → co2114-2026.1.2}/LICENSE +0 -0
- {co2114-2026.1.0 → co2114-2026.1.2}/README.md +0 -0
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114/__init__.py +0 -0
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114/agent/__init__.py +0 -0
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114/agent/things.py +0 -0
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114/constraints/__init__.py +0 -0
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114/constraints/csp/util.py +0 -0
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114/constraints/magic.py +0 -0
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114/constraints/sudoku.py +0 -0
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114/engine.py +0 -0
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114/optimisation/__init__.py +0 -0
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114/optimisation/minimax.py +0 -0
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114/reasoning/__init__.py +0 -0
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114/reasoning/cluedo.py +0 -0
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114/reasoning/inference.py +0 -0
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114/reasoning/logic.py +0 -0
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114/search/__init__.py +0 -0
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114/util/__init__.py +0 -0
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114/util/colours.py +0 -0
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114/util/fonts.py +0 -0
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114.egg-info/SOURCES.txt +0 -0
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114.egg-info/dependency_links.txt +0 -0
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114.egg-info/requires.txt +0 -0
- {co2114-2026.1.0 → co2114-2026.1.2}/co2114.egg-info/top_level.txt +0 -0
- {co2114-2026.1.0 → co2114-2026.1.2}/setup.cfg +0 -0
|
@@ -66,7 +66,7 @@ class BaseEnvironment:
|
|
|
66
66
|
|
|
67
67
|
for i in range(steps):
|
|
68
68
|
self.__counter += 1
|
|
69
|
-
if self.is_done: # print
|
|
69
|
+
if self.is_done: # print termination message and exit
|
|
70
70
|
print(f"{self}: Simulation complete after {i} of {steps} iterations.")
|
|
71
71
|
return
|
|
72
72
|
self.step() # else iterate one step
|
|
@@ -4,7 +4,12 @@ from ..agent.environment import GraphicEnvironment
|
|
|
4
4
|
from ..search.util import manhattan
|
|
5
5
|
import random
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
from typing import override
|
|
8
|
+
|
|
9
|
+
Location = tuple[int, int]
|
|
10
|
+
State = None | dict[str, dict[House | Hospital, Location] | dict[str, int]]
|
|
11
|
+
Numeric = int | float
|
|
12
|
+
PRESET_STATES: dict[str, dict[str, list[Location] | int]] = {
|
|
8
13
|
"empty": None,
|
|
9
14
|
'0': {
|
|
10
15
|
"hospitals": [(2, 4)],
|
|
@@ -41,35 +46,99 @@ PRESET_STATES = {
|
|
|
41
46
|
|
|
42
47
|
|
|
43
48
|
class HospitalOptimiser(Optimiser, UtilityBasedAgent):
|
|
44
|
-
|
|
49
|
+
""" Hospital Optimiser Agent"""
|
|
50
|
+
def explore(self, state:State) -> None:
|
|
51
|
+
""" Move hospitals to new locations in state
|
|
52
|
+
|
|
53
|
+
:param state: new state with hospital locations
|
|
54
|
+
"""
|
|
45
55
|
if not state: return
|
|
46
56
|
print(f"{self}: exploring state\n {state['hospitals']}")
|
|
47
57
|
for hospital, loc in state["hospitals"].items():
|
|
48
58
|
hospital.location = loc
|
|
49
59
|
|
|
50
|
-
|
|
51
|
-
|
|
60
|
+
@override
|
|
61
|
+
def utility(self, state:State) -> Numeric:
|
|
62
|
+
""" Calculate utility of possible state by calculating distance
|
|
63
|
+
of each hospital to houses
|
|
64
|
+
|
|
65
|
+
Returns negative total distance to be minimised.
|
|
66
|
+
|
|
67
|
+
:param state: current state with hospital and house locations
|
|
68
|
+
:return: negative total distance
|
|
69
|
+
"""
|
|
52
70
|
obj = 0
|
|
53
|
-
houses,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
71
|
+
houses: dict[House, Location] = state["houses"] # type: ignore
|
|
72
|
+
hospitals: dict[Hospital, Location] = state["hospitals"] # type: ignore
|
|
73
|
+
|
|
74
|
+
for house in houses: # iterate over houses
|
|
75
|
+
dist_to_nearest_hospital = infinity # very big
|
|
76
|
+
|
|
77
|
+
for hospital in hospitals: # iterate over hospitals
|
|
78
|
+
house_loc = houses[house]
|
|
79
|
+
hospital_loc = hospitals[hospital]
|
|
80
|
+
|
|
81
|
+
dist = manhattan(house_loc, hospital_loc)
|
|
82
|
+
|
|
83
|
+
# calculate closest distance
|
|
84
|
+
if dist < dist_to_nearest_hospital:
|
|
85
|
+
dist_to_nearest_hospital = dist
|
|
86
|
+
|
|
87
|
+
obj += dist_to_nearest_hospital # add distance for this house
|
|
63
88
|
return -obj
|
|
64
89
|
|
|
65
90
|
|
|
66
91
|
class HospitalPlacement(GraphicEnvironment):
|
|
67
|
-
|
|
92
|
+
""" Hospital Placement Environment
|
|
93
|
+
|
|
94
|
+
Allows placement of hospitals and houses on a grid
|
|
95
|
+
|
|
96
|
+
Has a state consisting of hospital and house locations and bounds of the environment
|
|
97
|
+
"""
|
|
98
|
+
def __init__(self,
|
|
99
|
+
init:dict[str, list[Location] | int] | None = None,
|
|
100
|
+
*args,
|
|
101
|
+
**kwargs) -> None:
|
|
102
|
+
""" Constroctor for HospitalPlacement environment
|
|
103
|
+
|
|
104
|
+
:param init: initial state dictionary with hospital and house locations and height/width of environment
|
|
105
|
+
:param args: additional args for GraphicEnvironment
|
|
106
|
+
:param kwargs: additional kwargs for GraphicEnvironment
|
|
107
|
+
"""
|
|
68
108
|
super().__init__(*args, **kwargs)
|
|
69
109
|
self.initialise_state(init)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def initialise_state(self,
|
|
113
|
+
state_dict: dict[str, list[Location] | int]) -> None:
|
|
114
|
+
""" Initialise environment state from state dictionary
|
|
115
|
+
|
|
116
|
+
:param state_dict: state dictionary with hospital and house locations and height/width of environment
|
|
117
|
+
"""
|
|
118
|
+
if state_dict is None: return # empty state
|
|
119
|
+
|
|
120
|
+
if "height" in state_dict:
|
|
121
|
+
self.height = state_dict["height"]
|
|
122
|
+
|
|
123
|
+
if "width" in state_dict:
|
|
124
|
+
self.width = state_dict["width"]
|
|
125
|
+
|
|
126
|
+
self.size = self.width, self.height
|
|
127
|
+
|
|
128
|
+
if "hospitals" in state_dict:
|
|
129
|
+
for loc in state_dict["hospitals"]:
|
|
130
|
+
self.add_thing(Hospital(), location=loc)
|
|
131
|
+
|
|
132
|
+
for loc in state_dict["houses"]:
|
|
133
|
+
self.add_thing(House(), location=loc)
|
|
134
|
+
|
|
70
135
|
|
|
71
136
|
@property
|
|
72
|
-
def state(self):
|
|
137
|
+
def state(self) -> State:
|
|
138
|
+
""" Attribute returning current environment state
|
|
139
|
+
|
|
140
|
+
:return: current state with hospital and house locations and bounds of environment
|
|
141
|
+
"""
|
|
73
142
|
return {
|
|
74
143
|
"hospitals": {
|
|
75
144
|
thing: thing.location
|
|
@@ -85,7 +154,12 @@ class HospitalPlacement(GraphicEnvironment):
|
|
|
85
154
|
}
|
|
86
155
|
|
|
87
156
|
@property
|
|
88
|
-
def neighbours(self):
|
|
157
|
+
def neighbours(self) -> list[State]:
|
|
158
|
+
""" Generate neighbouring states by moving each hospital
|
|
159
|
+
in each direction by one unit if possible.
|
|
160
|
+
|
|
161
|
+
:return: list of neighbouring states
|
|
162
|
+
"""
|
|
89
163
|
neighbours = []
|
|
90
164
|
for i, hospital in enumerate(self.state["hospitals"]):
|
|
91
165
|
location = hospital.location
|
|
@@ -97,17 +171,31 @@ class HospitalPlacement(GraphicEnvironment):
|
|
|
97
171
|
neighbours.append(candidate)
|
|
98
172
|
return neighbours
|
|
99
173
|
|
|
100
|
-
def is_inbounds(self, location):
|
|
174
|
+
def is_inbounds(self, location:Location) -> bool:
|
|
175
|
+
""" Checks if location is in bounds and unoccupied
|
|
176
|
+
|
|
177
|
+
:return bool: True if location is in bounds and unoccupied
|
|
178
|
+
"""
|
|
101
179
|
if not super().is_inbounds(location):
|
|
102
180
|
return False
|
|
103
181
|
return len(self.things_at(location)) == 0
|
|
104
182
|
|
|
105
|
-
|
|
183
|
+
@override
|
|
184
|
+
def add_agent(self, agent:Agent) -> None:
|
|
185
|
+
""" Add agent to environment, overrides GraphicEnvironment method as agent has no location.
|
|
186
|
+
|
|
187
|
+
:param agent: agent to add
|
|
188
|
+
"""
|
|
106
189
|
if not isinstance(agent, Agent):
|
|
107
190
|
raise TypeError(f"{self}: {agent} is not an Agent.")
|
|
108
191
|
self.agents.add(agent)
|
|
109
192
|
|
|
110
|
-
|
|
193
|
+
@override
|
|
194
|
+
def add_thing_randomly(self, thing:things.Thing) -> None:
|
|
195
|
+
""" Add thing to random unoccupied location in environment
|
|
196
|
+
|
|
197
|
+
:param thing: thing to add
|
|
198
|
+
"""
|
|
111
199
|
x = random.randint(self.x_start, self.x_end-1)
|
|
112
200
|
y = random.randint(self.y_start, self.y_end-1)
|
|
113
201
|
lim, count = 10, 0
|
|
@@ -120,35 +208,31 @@ class HospitalPlacement(GraphicEnvironment):
|
|
|
120
208
|
y = random.randint(self.y_start, self.y_end-1)
|
|
121
209
|
self.add_thing(thing, (x,y))
|
|
122
210
|
|
|
123
|
-
|
|
124
|
-
def initialise_state(self, state_dict):
|
|
125
|
-
if state_dict is None:
|
|
126
|
-
return
|
|
127
|
-
if "height" in state_dict:
|
|
128
|
-
self.height = state_dict["height"]
|
|
129
|
-
if "width" in state_dict:
|
|
130
|
-
self.width = state_dict["width"]
|
|
131
|
-
self.size = self.width, self.height
|
|
132
|
-
if "hospitals" in state_dict:
|
|
133
|
-
for loc in state_dict["hospitals"]:
|
|
134
|
-
self.add_thing(Hospital(), location=loc)
|
|
135
|
-
for loc in state_dict["houses"]:
|
|
136
|
-
self.add_thing(House(), location=loc)
|
|
137
|
-
|
|
138
|
-
|
|
139
211
|
@property
|
|
140
|
-
def is_done(self):
|
|
212
|
+
def is_done(self) -> bool:
|
|
213
|
+
""" Definition of when environment is done """
|
|
141
214
|
if len(self.agents) == 0: return True # if there are no agents
|
|
142
215
|
return hasattr(self, "success") and self.success
|
|
143
216
|
|
|
144
|
-
|
|
217
|
+
@override
|
|
218
|
+
def percept(self, agent:Agent) -> tuple[State, list[State]]:
|
|
219
|
+
""" Percept for agent in environment
|
|
220
|
+
|
|
221
|
+
:return tuple: current state and neighbouring states
|
|
222
|
+
"""
|
|
145
223
|
return self.state, self.neighbours
|
|
146
224
|
|
|
147
|
-
|
|
148
|
-
|
|
225
|
+
@override
|
|
226
|
+
def execute_action(self,
|
|
227
|
+
agent:HospitalOptimiser,action:tuple[str,State]) -> None:
|
|
228
|
+
""" Execute an action
|
|
229
|
+
|
|
230
|
+
:param agent: agent performing action
|
|
231
|
+
:param action: action to execute, tuple of command and state. possible commands are "done" and "explore"
|
|
232
|
+
"""
|
|
149
233
|
command, state = action
|
|
150
234
|
match command:
|
|
151
|
-
case "done":
|
|
235
|
+
case "done": # optimisation complete
|
|
152
236
|
if state:
|
|
153
237
|
agent.explore(state)
|
|
154
238
|
self.success = True
|
|
@@ -2,17 +2,26 @@ from ..search import things
|
|
|
2
2
|
|
|
3
3
|
from ..util.fonts import platform
|
|
4
4
|
|
|
5
|
+
from typing import override
|
|
6
|
+
|
|
7
|
+
# Re-export relevant classes from things module
|
|
5
8
|
Agent = things.Agent
|
|
6
9
|
UtilityBasedAgent = things.UtilityBasedAgent
|
|
7
10
|
|
|
8
11
|
class Hospital(things.Thing):
|
|
12
|
+
@override
|
|
9
13
|
def __repr__(self):
|
|
14
|
+
""" String representation of a Hospital. """
|
|
10
15
|
return "🏥" if platform != "darwin" else "+"
|
|
11
16
|
|
|
12
17
|
class House(things.Thing):
|
|
18
|
+
@override
|
|
13
19
|
def __repr__(self):
|
|
20
|
+
""" String representation of a House. """
|
|
14
21
|
return "🏠" if platform != "darwin" else "^"
|
|
15
22
|
|
|
16
23
|
class Optimiser(things.Agent):
|
|
24
|
+
@override
|
|
17
25
|
def __repr__(self):
|
|
26
|
+
""" String representation of an Optimiser. """
|
|
18
27
|
return "📈"
|
|
@@ -207,7 +207,11 @@ class GraphEnvironment(Environment):
|
|
|
207
207
|
|
|
208
208
|
@override
|
|
209
209
|
def percept(self, agent:Agent) -> list[tuple[Node, Numeric]]:
|
|
210
|
-
""" Return the percepts for the agent based on its location in the graph.
|
|
210
|
+
""" Return the percepts for the agent based on its location in the graph.
|
|
211
|
+
|
|
212
|
+
:param agent: Agent for which to get percepts.
|
|
213
|
+
:return: List of tuples (neighbouring node, edge weight). Weight is 1 if unweighted
|
|
214
|
+
"""
|
|
211
215
|
node:Node = agent.location # type: ignore
|
|
212
216
|
if len(node.weights) == 0:
|
|
213
217
|
return [(n, 1) for n in node.neighbours]
|
|
@@ -254,7 +258,7 @@ class GraphEnvironment(Environment):
|
|
|
254
258
|
super().add_agent(agent)
|
|
255
259
|
|
|
256
260
|
|
|
257
|
-
def show(self, *args, **kwargs):
|
|
261
|
+
def show(self, *args, **kwargs) -> Figure:
|
|
258
262
|
""" Show a plot of the graph environment. """
|
|
259
263
|
with warnings.catch_warnings():
|
|
260
264
|
warnings.simplefilter("ignore")
|
|
@@ -2,6 +2,8 @@ from collections.abc import Iterable
|
|
|
2
2
|
import warnings
|
|
3
3
|
from matplotlib import pyplot as plt
|
|
4
4
|
from collections import deque
|
|
5
|
+
from typing import override
|
|
6
|
+
from matplotlib.figure import Figure # for type hinting
|
|
5
7
|
|
|
6
8
|
with warnings.catch_warnings():
|
|
7
9
|
warnings.simplefilter("ignore")
|
|
@@ -34,6 +36,22 @@ PRESET_MAZES = {
|
|
|
34
36
|
"x x xx xx ",
|
|
35
37
|
"xxx x xxxx ",
|
|
36
38
|
" xx "],
|
|
39
|
+
3: ["xxx xxxxxxxxx",
|
|
40
|
+
"x xxxxxxxxxxxxxxxxxxx x x",
|
|
41
|
+
"x xxxx x x x x",
|
|
42
|
+
"x xxxxxxxxxxxxxxxxxxx x x x x",
|
|
43
|
+
"x x x x x",
|
|
44
|
+
"xxxxxxxxxxxxxxxxxxxxx x x x x",
|
|
45
|
+
"x xx x x x x",
|
|
46
|
+
"x x xx xxx xx xxxxxxxxx x x x",
|
|
47
|
+
"x x x xxox x x x",
|
|
48
|
+
"x x xx xxxxxxxxxxxxxxxx x x x",
|
|
49
|
+
"xxx xx xxxx x x x",
|
|
50
|
+
"xxx xxxxxxxxxxxxxx xx x x x x",
|
|
51
|
+
"xxx xx x x x x",
|
|
52
|
+
"xxxxxx xxxxxxxx xxxxxxx x x x",
|
|
53
|
+
"xxxxxx xxxx x x",
|
|
54
|
+
" xxxxxxxxxxxxxxxxxxxxxx"],
|
|
37
55
|
4: ["xxx xxxxxxxxx",
|
|
38
56
|
"x xxxxxxxxxxxxxxxxxxx x x",
|
|
39
57
|
"x xxxx x x x x",
|
|
@@ -56,27 +74,39 @@ PRESET_STARTS = {
|
|
|
56
74
|
0: (3, 0),
|
|
57
75
|
1: (3, 0),
|
|
58
76
|
2: (7, 0),
|
|
77
|
+
3: (1, 27),
|
|
59
78
|
4: (15, 0)
|
|
60
79
|
}
|
|
61
80
|
|
|
81
|
+
|
|
62
82
|
class MazeTile(Node):
|
|
63
|
-
def __init__(self, passable:bool):
|
|
83
|
+
def __init__(self, passable:bool) -> None:
|
|
84
|
+
""" MazeTile - A tile in a maze, either passable or not.
|
|
85
|
+
|
|
86
|
+
:param passable: Whether the tile is passable.
|
|
87
|
+
"""
|
|
64
88
|
super().__init__()
|
|
65
|
-
self.is_passable = passable
|
|
66
|
-
self.is_goal = False
|
|
67
|
-
self.visited = False
|
|
89
|
+
self.is_passable:bool = passable
|
|
90
|
+
self.is_goal:bool = False
|
|
91
|
+
self.visited:bool = False
|
|
68
92
|
|
|
69
|
-
def __repr__(self):
|
|
93
|
+
def __repr__(self) -> str:
|
|
94
|
+
""" Str representation of the MazeTile. """
|
|
70
95
|
repr = "⬜" if self.is_passable else "⬛"
|
|
71
96
|
if hasattr(self, "location"):
|
|
72
97
|
repr += str(self.location)
|
|
73
98
|
return repr
|
|
74
99
|
|
|
75
100
|
|
|
101
|
+
|
|
76
102
|
class Maze(Graph):
|
|
77
|
-
def __init__(self, template=None
|
|
103
|
+
def __init__(self, template:list[str]|None=None) -> None:
|
|
104
|
+
""" Maze class: a grid-based maze structure.
|
|
105
|
+
|
|
106
|
+
:param template: Optional template to generate the maze from.
|
|
107
|
+
"""
|
|
78
108
|
super().__init__()
|
|
79
|
-
if template is not None:
|
|
109
|
+
if template is not None: # generate from template
|
|
80
110
|
if not self._is_valid_template(template):
|
|
81
111
|
raise ValueError(f"{self}: template provided is not valid")
|
|
82
112
|
self.template = template
|
|
@@ -84,11 +114,21 @@ class Maze(Graph):
|
|
|
84
114
|
self.size = self.width, self.height
|
|
85
115
|
self._generate_from_template()
|
|
86
116
|
|
|
87
|
-
|
|
117
|
+
@override
|
|
118
|
+
def __repr__(self) -> str:
|
|
119
|
+
""" String representation of the Maze. """
|
|
88
120
|
return "ꡙ"
|
|
89
121
|
|
|
90
|
-
def _is_valid_template(self, template):
|
|
122
|
+
def _is_valid_template(self, template:list[str]) -> bool:
|
|
123
|
+
""" Checks if the provided template is valid.
|
|
124
|
+
|
|
125
|
+
A valid template is a list of strings, all of the same length, containing only " ", "o", or "x".
|
|
126
|
+
|
|
127
|
+
:param template: The template to validate.
|
|
128
|
+
:return valid: Whether the template is valid.
|
|
129
|
+
"""
|
|
91
130
|
def valid(row, size):
|
|
131
|
+
""" Internal row validator. """
|
|
92
132
|
return False if len(row) != size else not any(
|
|
93
133
|
c not in (' ','o','x') for c in row)
|
|
94
134
|
|
|
@@ -98,7 +138,11 @@ class Maze(Graph):
|
|
|
98
138
|
return all(valid(row,width) for row in template)
|
|
99
139
|
return False
|
|
100
140
|
|
|
101
|
-
def _generate_from_template(self):
|
|
141
|
+
def _generate_from_template(self) -> None:
|
|
142
|
+
""" Generates the maze from the provided template.
|
|
143
|
+
|
|
144
|
+
Uses internal self.template to build the maze grid.
|
|
145
|
+
"""
|
|
102
146
|
self.grid = []
|
|
103
147
|
w,h = self.width, self.height
|
|
104
148
|
print(f"{self}: generating {w}x{h} maze from template")
|
|
@@ -117,70 +161,127 @@ class Maze(Graph):
|
|
|
117
161
|
self.grid.append(grid_row)
|
|
118
162
|
self.add_node(self.grid[0][0]) # cascade adding nodes
|
|
119
163
|
|
|
120
|
-
|
|
121
|
-
def plot_nodes(self
|
|
164
|
+
@override
|
|
165
|
+
def plot_nodes(self) -> Figure:
|
|
166
|
+
""" Plots the maze nodes using matplotlib."""
|
|
122
167
|
labels = {self.grid[i][j]: f"({i},{j})"
|
|
123
168
|
for i in range(self.width)
|
|
124
169
|
for j in range(self.height)}
|
|
125
170
|
condition = lambda node: node.is_passable
|
|
126
|
-
return super().plot_nodes(init=
|
|
171
|
+
return super().plot_nodes(init=None, labels=labels, condition=condition)
|
|
172
|
+
|
|
127
173
|
|
|
128
174
|
|
|
129
175
|
class MazeEnvironment(GraphEnvironment):
|
|
130
|
-
|
|
176
|
+
""" MazeEnvironment - An environment for maze navigation.
|
|
177
|
+
|
|
178
|
+
A type of GraphEnvironment specifically for Maze graphs.
|
|
179
|
+
"""
|
|
180
|
+
def __init__(self, maze:Maze, *args, **kwargs) -> None:
|
|
181
|
+
""" Initialises the MazeEnvironment.
|
|
182
|
+
|
|
183
|
+
:param maze: The Maze to be used as the environment.
|
|
184
|
+
:param args: Additional positional arguments for GraphEnvironment.
|
|
185
|
+
:param kwargs: Additional keyword arguments for GraphEnvironment.
|
|
186
|
+
"""
|
|
131
187
|
if not isinstance(maze, Maze):
|
|
132
188
|
raise TypeError(f"{self}: maze must be valid Maze")
|
|
133
189
|
super().__init__(maze, *args, **kwargs)
|
|
134
190
|
self.size = self.width, self.height = maze.size
|
|
135
191
|
self.success = False # maze is solved
|
|
136
|
-
|
|
192
|
+
|
|
193
|
+
@override
|
|
194
|
+
def __repr__(self) -> str:
|
|
195
|
+
""" String representation of the MazeEnvironment. """
|
|
137
196
|
return self.maze.__repr__()
|
|
138
197
|
|
|
139
198
|
@property
|
|
140
|
-
def maze(self):
|
|
199
|
+
def maze(self) -> Maze:
|
|
200
|
+
""" Returns the maze graph of the environment. """
|
|
141
201
|
return self.graph
|
|
142
202
|
|
|
143
203
|
@property
|
|
144
|
-
def grid(self):
|
|
204
|
+
def grid(self) -> list[list[MazeTile]]:
|
|
205
|
+
""" Returns the maze grid as a nested list of MazeTiles. """
|
|
145
206
|
return self.maze.grid
|
|
146
207
|
|
|
147
208
|
@property
|
|
148
|
-
def is_done(self):
|
|
209
|
+
def is_done(self) -> bool:
|
|
210
|
+
""" Returns whether the maze has been solved. """
|
|
149
211
|
if len(self.agents) == 0: return True
|
|
150
212
|
return self.success
|
|
151
213
|
|
|
152
214
|
@property
|
|
153
|
-
def goal(self):
|
|
215
|
+
def goal(self) -> MazeTile|None:
|
|
216
|
+
""" Returns the goal tile of the maze, if there is one.
|
|
217
|
+
"""
|
|
154
218
|
goal = {node for node in self.maze.nodes if node.is_goal}
|
|
155
219
|
if len(goal) == 1:
|
|
156
220
|
goal = next(iter(goal))
|
|
221
|
+
else:
|
|
222
|
+
goal = None
|
|
157
223
|
return goal
|
|
158
224
|
|
|
159
|
-
|
|
225
|
+
|
|
226
|
+
def show_graph(self, *args, **kwargs) -> Figure:
|
|
227
|
+
""" Wrapper for GraphEnvironment.show() """
|
|
160
228
|
return self.show(*args, **kwargs)
|
|
161
229
|
|
|
162
|
-
|
|
230
|
+
|
|
231
|
+
@override
|
|
232
|
+
def percept(self, agent:Agent) -> set[MazeTile]:
|
|
233
|
+
""" Returns the percepts for the agent: neighbouring nodes and their weights.
|
|
234
|
+
|
|
235
|
+
:param agent: The agent to get percepts for.
|
|
236
|
+
:return percepts: List of tuples of (neighbouring maze tile, weight).
|
|
237
|
+
"""
|
|
163
238
|
node = agent.location
|
|
164
239
|
return node.neighbours
|
|
165
240
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
241
|
+
@override
|
|
242
|
+
def execute_action(self, agent:Agent, action:tuple[str, MazeTile]) -> None:
|
|
243
|
+
""" Executes the agent's action in the environment.
|
|
244
|
+
|
|
245
|
+
:param agent: The agent performing the action.
|
|
246
|
+
:param action: The action to be performed, as a tuple of (command, MazeTile). Command should be "move".
|
|
247
|
+
"""
|
|
248
|
+
command, node = action # e.g. ("move", node)
|
|
169
249
|
if command == "move":
|
|
170
250
|
if node in self.maze:
|
|
171
251
|
self.success = agent.move_to(node)
|
|
172
252
|
|
|
173
|
-
|
|
253
|
+
@override
|
|
254
|
+
def run(self, *args, **kwargs) -> None:
|
|
255
|
+
""" Run the environment for a large number of steps by default.
|
|
256
|
+
|
|
257
|
+
Non-graphical default.
|
|
258
|
+
"""
|
|
174
259
|
super().run(10000, *args, **kwargs)
|
|
175
260
|
|
|
176
261
|
@classmethod
|
|
177
|
-
def from_template(MazeEnvironment, template):
|
|
262
|
+
def from_template(MazeEnvironment, template:list[str]) -> "MazeEnvironment":
|
|
263
|
+
""" Creates a MazeEnvironment from a template.
|
|
264
|
+
|
|
265
|
+
Returns instance of MazeEnvironment built from the provided template.
|
|
266
|
+
:param template: The template to build the maze from.
|
|
267
|
+
:return environment: The MazeEnvironment instance.
|
|
268
|
+
"""
|
|
178
269
|
maze = Maze(template)
|
|
179
270
|
return MazeEnvironment(maze)
|
|
180
271
|
|
|
181
272
|
|
|
182
273
|
class GraphicMaze(MazeEnvironment):
|
|
183
|
-
|
|
274
|
+
""" Graphical Maze Environment: A MazeEnvironment with graphical rendering capabilities. Uses MazeApp to render.
|
|
275
|
+
"""
|
|
276
|
+
@override
|
|
277
|
+
def run(self, graphical=True, lps=2, *args, **kwargs) -> None:
|
|
278
|
+
""" Run the MazeEnvironment in graphical or non-graphical mode.
|
|
279
|
+
|
|
280
|
+
:param graphical: Whether to run in graphical mode.
|
|
281
|
+
:param lps: The number of logic updates per second in graphical mode.
|
|
282
|
+
:param args: Additional positional arguments for Engine.
|
|
283
|
+
:param kwargs: Additional keyword arguments for Engine.
|
|
284
|
+
"""
|
|
184
285
|
if graphical:
|
|
185
286
|
MazeApp(self, *args, name=f"{self}", lps=lps, **kwargs).run()
|
|
186
287
|
else:
|
|
@@ -189,8 +290,18 @@ class GraphicMaze(MazeEnvironment):
|
|
|
189
290
|
|
|
190
291
|
class MazeApp(App):
|
|
191
292
|
""" MazeApp
|
|
192
|
-
Graphical version of Maze
|
|
193
|
-
|
|
293
|
+
Graphical version of Maze
|
|
294
|
+
"""
|
|
295
|
+
def __init__(self,
|
|
296
|
+
environment:MazeEnvironment|None = None,
|
|
297
|
+
track_agent = False,
|
|
298
|
+
**kwargs) -> None:
|
|
299
|
+
""" Construct MazeApp
|
|
300
|
+
|
|
301
|
+
:param environment: The MazeEnvironment to be rendered. If None, a default maze is created.
|
|
302
|
+
:param track_agent: Whether to visually track the agent's path, default False
|
|
303
|
+
:param kwargs: Additional keyword arguments for App.
|
|
304
|
+
"""
|
|
194
305
|
if environment is None:
|
|
195
306
|
print(f"{self}: building new MazeEnvironment from template")
|
|
196
307
|
environment = MazeEnvironment.from_template(PRESET_MAZES[0])
|
|
@@ -208,7 +319,8 @@ class MazeApp(App):
|
|
|
208
319
|
self._flag = True
|
|
209
320
|
self.track = track_agent
|
|
210
321
|
|
|
211
|
-
|
|
322
|
+
|
|
323
|
+
def _fit_to_environment(self) -> None:
|
|
212
324
|
""" Fit width and height ratios to match environment """
|
|
213
325
|
_flag = self.environment.width > self.environment.height
|
|
214
326
|
if _flag:
|
|
@@ -225,8 +337,9 @@ class MazeApp(App):
|
|
|
225
337
|
self.height, self.width = (a,b) if _flag else (b,a)
|
|
226
338
|
self.size = self.width, self.height
|
|
227
339
|
|
|
228
|
-
|
|
229
|
-
def update(self):
|
|
340
|
+
@override
|
|
341
|
+
def update(self) -> None:
|
|
342
|
+
""" Update MazeApp by iterating underlying simulation environment """
|
|
230
343
|
if self.counter < 0:
|
|
231
344
|
self.counter += 1
|
|
232
345
|
return
|
|
@@ -237,12 +350,15 @@ class MazeApp(App):
|
|
|
237
350
|
print(f"{self.environment}: Maze complete after {self.counter} iterations.")
|
|
238
351
|
self._flag = False
|
|
239
352
|
|
|
240
|
-
|
|
353
|
+
@override
|
|
354
|
+
def render(self) -> None:
|
|
355
|
+
""" Render frame of MazeApp """
|
|
241
356
|
# self.screen.fill(self.environment.color)
|
|
242
357
|
self.render_grid()
|
|
243
358
|
self.render_things()
|
|
244
359
|
|
|
245
|
-
def render_grid(self):
|
|
360
|
+
def render_grid(self) -> None:
|
|
361
|
+
""" Render maze grid showing passable and non-passable tiles in light and dark red respectively. If the agent is tracked, visited tiles are shown in light green. """
|
|
246
362
|
nx,ny = self.environment.width, self.environment.height
|
|
247
363
|
self.tile_size = self.width / nx
|
|
248
364
|
tile_origin = (0,0)
|
|
@@ -274,7 +390,16 @@ class MazeApp(App):
|
|
|
274
390
|
tiles.append(row)
|
|
275
391
|
self.tiles = tiles
|
|
276
392
|
|
|
277
|
-
def label_tile(self,
|
|
393
|
+
def label_tile(self,
|
|
394
|
+
node:MazeTile,
|
|
395
|
+
string:str,
|
|
396
|
+
color:tuple[int,int,int] = COLOR_WHITE) -> None:
|
|
397
|
+
""" Labels a tile with a string at the center of the tile.
|
|
398
|
+
|
|
399
|
+
:param node: The MazeTile to label.
|
|
400
|
+
:param string: The string to label the tile with.
|
|
401
|
+
:param color: The color of the string.
|
|
402
|
+
"""
|
|
278
403
|
i,j = node.location
|
|
279
404
|
renderLoc = self.tiles[i][j].center
|
|
280
405
|
thingRender = self.thing_font.render(string, True, color)
|
|
@@ -282,7 +407,11 @@ class MazeApp(App):
|
|
|
282
407
|
thingRect.center = renderLoc
|
|
283
408
|
self.screen.blit(thingRender, thingRect)
|
|
284
409
|
|
|
285
|
-
def render_things(self):
|
|
410
|
+
def render_things(self) -> None:
|
|
411
|
+
""" Renders things to the maze environment.
|
|
412
|
+
|
|
413
|
+
Marks the goal tile with a crown emoji, agents with their string representation, and if tracking is enabled, marks frontier nodes with a question mark.
|
|
414
|
+
"""
|
|
286
415
|
locations = {}
|
|
287
416
|
for node in self.environment.maze:
|
|
288
417
|
if node.is_goal:
|
|
@@ -295,5 +424,4 @@ class MazeApp(App):
|
|
|
295
424
|
if self.track:
|
|
296
425
|
if hasattr(agent, "frontier"):
|
|
297
426
|
for node in agent.frontier:
|
|
298
|
-
self.label_tile(node, "?", COLOR_WHITE)
|
|
299
|
-
|
|
427
|
+
self.label_tile(node, "?", COLOR_WHITE)
|
|
@@ -14,7 +14,7 @@ class MazeRunner(Agent):
|
|
|
14
14
|
class GoalBasedAgent(ModelBasedAgent):
|
|
15
15
|
""" Base class for goal based agents
|
|
16
16
|
|
|
17
|
-
Requires
|
|
17
|
+
Requires implementation of methods:
|
|
18
18
|
`program(percepts:Collection[Thing]) -> Action`
|
|
19
19
|
`at_goal() -> bool`
|
|
20
20
|
"""
|
|
@@ -27,7 +27,7 @@ class GoalBasedAgent(ModelBasedAgent):
|
|
|
27
27
|
class UtilityBasedAgent(GoalBasedAgent):
|
|
28
28
|
""" Base class for utility based agents
|
|
29
29
|
|
|
30
|
-
Requires
|
|
30
|
+
Requires implementation of methods:
|
|
31
31
|
`program(percepts:Collection[Thing]) -> Action`
|
|
32
32
|
`at_goal() -> bool`
|
|
33
33
|
`utility(action:Action) -> float`
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from collections import deque
|
|
2
|
+
from typing import override, Iterator, TypeVar
|
|
3
|
+
|
|
4
|
+
GenericType = TypeVar('GenericType')
|
|
5
|
+
Numeric = int | float
|
|
6
|
+
|
|
7
|
+
class queue[GenericType]:
|
|
8
|
+
""" Wrapper for deque to provide FIFO queue functionality """
|
|
9
|
+
def __init__(self) -> None:
|
|
10
|
+
self.data:deque[GenericType] = deque() # internal deque storage
|
|
11
|
+
|
|
12
|
+
@override
|
|
13
|
+
def __repr__(self) -> str:
|
|
14
|
+
""" string representation of queue as sequence of elements """
|
|
15
|
+
return str(list(self.data))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def __iter__(self) -> Iterator[GenericType]:
|
|
19
|
+
""" Iterator over queue elements, allows in checks and looping """
|
|
20
|
+
return iter(self.data)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def push(self, x:GenericType) -> None:
|
|
24
|
+
""" Appends an item to the end of the queue
|
|
25
|
+
|
|
26
|
+
:param x: item to append
|
|
27
|
+
"""
|
|
28
|
+
self.data.append(x)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def pop(self) -> GenericType:
|
|
32
|
+
""" Pops first element """
|
|
33
|
+
return self.data.popleft()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class stack[GenericType](queue):
|
|
38
|
+
""" Wrapper for deque to provide LIFO stack functionality """
|
|
39
|
+
@override
|
|
40
|
+
def pop(self) -> GenericType:
|
|
41
|
+
""" Pops last element """
|
|
42
|
+
return self.data.pop()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def manhattan(a: tuple[Numeric, Numeric], b:tuple[Numeric, Numeric]) -> Numeric:
|
|
46
|
+
""" Basic Manhattan distance calculation between two (x,y) points """
|
|
47
|
+
ax, ay = a # (x, y)
|
|
48
|
+
bx, by = b # (x, y)
|
|
49
|
+
return abs(bx-ax) + abs(by-ay)
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
from collections import deque
|
|
2
|
-
|
|
3
|
-
class queue:
|
|
4
|
-
def __init__(self):
|
|
5
|
-
self.data = deque()
|
|
6
|
-
|
|
7
|
-
def __repr__(self):
|
|
8
|
-
return str(list(self.data))
|
|
9
|
-
|
|
10
|
-
def __iter__(self):
|
|
11
|
-
return iter(self.data)
|
|
12
|
-
|
|
13
|
-
def push(self, x):
|
|
14
|
-
self.data.append(x)
|
|
15
|
-
|
|
16
|
-
def pop(self):
|
|
17
|
-
return self.data.popleft()
|
|
18
|
-
|
|
19
|
-
class stack(queue):
|
|
20
|
-
def pop(self):
|
|
21
|
-
return self.data.pop()
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def manhattan(a, b):
|
|
25
|
-
ax, ay = a # (x, y)
|
|
26
|
-
bx, by = b # (x, y)
|
|
27
|
-
return abs(bx-ax) + abs(by-ay)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|