co2114 2026.1.0__tar.gz → 2026.1.1__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.
Files changed (37) hide show
  1. {co2114-2026.1.0 → co2114-2026.1.1}/PKG-INFO +1 -1
  2. {co2114-2026.1.0 → co2114-2026.1.1}/co2114/constraints/csp/__init__.py +4 -0
  3. {co2114-2026.1.0 → co2114-2026.1.1}/co2114/search/graph.py +6 -2
  4. {co2114-2026.1.0 → co2114-2026.1.1}/co2114/search/maze.py +166 -38
  5. {co2114-2026.1.0 → co2114-2026.1.1}/co2114/search/things.py +2 -2
  6. co2114-2026.1.1/co2114/search/util.py +49 -0
  7. {co2114-2026.1.0 → co2114-2026.1.1}/co2114.egg-info/PKG-INFO +1 -1
  8. {co2114-2026.1.0 → co2114-2026.1.1}/setup.py +1 -1
  9. co2114-2026.1.0/co2114/search/util.py +0 -27
  10. {co2114-2026.1.0 → co2114-2026.1.1}/LICENSE +0 -0
  11. {co2114-2026.1.0 → co2114-2026.1.1}/README.md +0 -0
  12. {co2114-2026.1.0 → co2114-2026.1.1}/co2114/__init__.py +0 -0
  13. {co2114-2026.1.0 → co2114-2026.1.1}/co2114/agent/__init__.py +0 -0
  14. {co2114-2026.1.0 → co2114-2026.1.1}/co2114/agent/environment.py +0 -0
  15. {co2114-2026.1.0 → co2114-2026.1.1}/co2114/agent/things.py +0 -0
  16. {co2114-2026.1.0 → co2114-2026.1.1}/co2114/constraints/__init__.py +0 -0
  17. {co2114-2026.1.0 → co2114-2026.1.1}/co2114/constraints/csp/util.py +0 -0
  18. {co2114-2026.1.0 → co2114-2026.1.1}/co2114/constraints/magic.py +0 -0
  19. {co2114-2026.1.0 → co2114-2026.1.1}/co2114/constraints/sudoku.py +0 -0
  20. {co2114-2026.1.0 → co2114-2026.1.1}/co2114/engine.py +0 -0
  21. {co2114-2026.1.0 → co2114-2026.1.1}/co2114/optimisation/__init__.py +0 -0
  22. {co2114-2026.1.0 → co2114-2026.1.1}/co2114/optimisation/minimax.py +0 -0
  23. {co2114-2026.1.0 → co2114-2026.1.1}/co2114/optimisation/planning.py +0 -0
  24. {co2114-2026.1.0 → co2114-2026.1.1}/co2114/optimisation/things.py +0 -0
  25. {co2114-2026.1.0 → co2114-2026.1.1}/co2114/reasoning/__init__.py +0 -0
  26. {co2114-2026.1.0 → co2114-2026.1.1}/co2114/reasoning/cluedo.py +0 -0
  27. {co2114-2026.1.0 → co2114-2026.1.1}/co2114/reasoning/inference.py +0 -0
  28. {co2114-2026.1.0 → co2114-2026.1.1}/co2114/reasoning/logic.py +0 -0
  29. {co2114-2026.1.0 → co2114-2026.1.1}/co2114/search/__init__.py +0 -0
  30. {co2114-2026.1.0 → co2114-2026.1.1}/co2114/util/__init__.py +0 -0
  31. {co2114-2026.1.0 → co2114-2026.1.1}/co2114/util/colours.py +0 -0
  32. {co2114-2026.1.0 → co2114-2026.1.1}/co2114/util/fonts.py +0 -0
  33. {co2114-2026.1.0 → co2114-2026.1.1}/co2114.egg-info/SOURCES.txt +0 -0
  34. {co2114-2026.1.0 → co2114-2026.1.1}/co2114.egg-info/dependency_links.txt +0 -0
  35. {co2114-2026.1.0 → co2114-2026.1.1}/co2114.egg-info/requires.txt +0 -0
  36. {co2114-2026.1.0 → co2114-2026.1.1}/co2114.egg-info/top_level.txt +0 -0
  37. {co2114-2026.1.0 → co2114-2026.1.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: co2114
3
- Version: 2026.1.0
3
+ Version: 2026.1.1
4
4
  Summary: codebase for co2114
5
5
  Author: wil ward
6
6
  Requires-Python: >=3.12
@@ -31,6 +31,10 @@ class ConstraintSatisfactionProblem:
31
31
 
32
32
  @property
33
33
  def variables(self):
34
+ """ List of variables in the CSP.
35
+
36
+ :return: List of variables.
37
+ """
34
38
  return self.__variables
35
39
 
36
40
  @property
@@ -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, size=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
- def __repr__(self):
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, init=None):
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=init, labels=labels, condition=condition)
171
+ return super().plot_nodes(init=None, labels=labels, condition=condition)
172
+
127
173
 
128
174
 
129
175
  class MazeEnvironment(GraphEnvironment):
130
- def __init__(self, maze, *args, **kwargs):
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
- def __repr__(self):
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
- def show_graph(self, *args, **kwargs):
225
+
226
+ def show_graph(self, *args, **kwargs) -> Figure:
227
+ """ Wrapper for GraphEnvironment.show() """
160
228
  return self.show(*args, **kwargs)
161
229
 
162
- def percept(self, agent):
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
- def execute_action(self, agent, action):
167
- """ """
168
- command, node = action
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
- def run(self, track_agent=False, *args, **kwargs):
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
- def run(self, graphical=True, lps=2, *args, **kwargs):
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
- def __init__(self, environment:MazeEnvironment=None, track_agent=False,**kwargs):
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
- def _fit_to_environment(self):
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
- def render(self):
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, node, string, color=COLOR_WHITE):
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 implemnentation of methods:
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 implemnentation of methods:
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: co2114
3
- Version: 2026.1.0
3
+ Version: 2026.1.1
4
4
  Summary: codebase for co2114
5
5
  Author: wil ward
6
6
  Requires-Python: >=3.12
@@ -2,7 +2,7 @@ from setuptools import setup
2
2
 
3
3
  setup(
4
4
  name="co2114",
5
- version="2026.1.0",
5
+ version="2026.1.1",
6
6
  description="codebase for co2114",
7
7
  author="wil ward",
8
8
  python_requires=">=3.12",
@@ -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