co2114 2025.2.2__tar.gz → 2026.0.3__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 (41) hide show
  1. {co2114-2025.2.2 → co2114-2026.0.3}/PKG-INFO +4 -3
  2. co2114-2026.0.3/co2114/agent/environment.py +467 -0
  3. co2114-2026.0.3/co2114/agent/things.py +92 -0
  4. {co2114-2025.2.2 → co2114-2026.0.3}/co2114/engine.py +114 -37
  5. co2114-2026.0.3/co2114/search/graph.py +445 -0
  6. co2114-2026.0.3/co2114/search/things.py +56 -0
  7. co2114-2026.0.3/co2114/util/__init__.py +53 -0
  8. {co2114-2025.2.2 → co2114-2026.0.3}/co2114.egg-info/PKG-INFO +4 -3
  9. {co2114-2025.2.2 → co2114-2026.0.3}/setup.py +2 -2
  10. co2114-2025.2.2/co2114/agent/environment.py +0 -301
  11. co2114-2025.2.2/co2114/agent/things.py +0 -68
  12. co2114-2025.2.2/co2114/search/graph.py +0 -296
  13. co2114-2025.2.2/co2114/search/things.py +0 -32
  14. co2114-2025.2.2/co2114/util/__init__.py +0 -29
  15. {co2114-2025.2.2 → co2114-2026.0.3}/LICENSE +0 -0
  16. {co2114-2025.2.2 → co2114-2026.0.3}/README.md +0 -0
  17. {co2114-2025.2.2 → co2114-2026.0.3}/co2114/__init__.py +0 -0
  18. {co2114-2025.2.2 → co2114-2026.0.3}/co2114/agent/__init__.py +0 -0
  19. {co2114-2025.2.2 → co2114-2026.0.3}/co2114/constraints/__init__.py +0 -0
  20. {co2114-2025.2.2 → co2114-2026.0.3}/co2114/constraints/csp/__init__.py +0 -0
  21. {co2114-2025.2.2 → co2114-2026.0.3}/co2114/constraints/csp/util.py +0 -0
  22. {co2114-2025.2.2 → co2114-2026.0.3}/co2114/constraints/magic.py +0 -0
  23. {co2114-2025.2.2 → co2114-2026.0.3}/co2114/constraints/sudoku.py +0 -0
  24. {co2114-2025.2.2 → co2114-2026.0.3}/co2114/optimisation/__init__.py +0 -0
  25. {co2114-2025.2.2 → co2114-2026.0.3}/co2114/optimisation/minimax.py +0 -0
  26. {co2114-2025.2.2 → co2114-2026.0.3}/co2114/optimisation/planning.py +0 -0
  27. {co2114-2025.2.2 → co2114-2026.0.3}/co2114/optimisation/things.py +0 -0
  28. {co2114-2025.2.2 → co2114-2026.0.3}/co2114/reasoning/__init__.py +0 -0
  29. {co2114-2025.2.2 → co2114-2026.0.3}/co2114/reasoning/cluedo.py +0 -0
  30. {co2114-2025.2.2 → co2114-2026.0.3}/co2114/reasoning/inference.py +0 -0
  31. {co2114-2025.2.2 → co2114-2026.0.3}/co2114/reasoning/logic.py +0 -0
  32. {co2114-2025.2.2 → co2114-2026.0.3}/co2114/search/__init__.py +0 -0
  33. {co2114-2025.2.2 → co2114-2026.0.3}/co2114/search/maze.py +0 -0
  34. {co2114-2025.2.2 → co2114-2026.0.3}/co2114/search/util.py +0 -0
  35. {co2114-2025.2.2 → co2114-2026.0.3}/co2114/util/colours.py +0 -0
  36. {co2114-2025.2.2 → co2114-2026.0.3}/co2114/util/fonts.py +0 -0
  37. {co2114-2025.2.2 → co2114-2026.0.3}/co2114.egg-info/SOURCES.txt +0 -0
  38. {co2114-2025.2.2 → co2114-2026.0.3}/co2114.egg-info/dependency_links.txt +0 -0
  39. {co2114-2025.2.2 → co2114-2026.0.3}/co2114.egg-info/requires.txt +0 -0
  40. {co2114-2025.2.2 → co2114-2026.0.3}/co2114.egg-info/top_level.txt +0 -0
  41. {co2114-2025.2.2 → co2114-2026.0.3}/setup.cfg +0 -0
@@ -1,9 +1,9 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: co2114
3
- Version: 2025.2.2
3
+ Version: 2026.0.3
4
4
  Summary: codebase for co2114
5
5
  Author: wil ward
6
- Requires-Python: >=3.10
6
+ Requires-Python: >=3.12
7
7
  License-File: LICENSE
8
8
  Requires-Dist: pygame>=2.6
9
9
  Requires-Dist: numpy
@@ -13,6 +13,7 @@ Requires-Dist: jupyter
13
13
  Requires-Dist: scikit-learn
14
14
  Requires-Dist: seaborn
15
15
  Dynamic: author
16
+ Dynamic: license-file
16
17
  Dynamic: requires-dist
17
18
  Dynamic: requires-python
18
19
  Dynamic: summary
@@ -0,0 +1,467 @@
1
+ """ENVIRONMENT.PY
2
+ This contains code for agent environments and simulations
3
+ """
4
+ import warnings
5
+ import math
6
+ import random
7
+ from collections.abc import Collection, Iterable
8
+ from typing import Union, TypeVar, override
9
+
10
+ with warnings.catch_warnings():
11
+ warnings.simplefilter("ignore")
12
+ import pygame
13
+
14
+ from ..engine import App
15
+ from ..util.colours import COLOR_BLACK, COLOR_WHITE
16
+ from ..util.fonts import _get_symbolic_font_unsafe
17
+ from .things import Thing, Agent, Obstacle, Action
18
+
19
+ SYMBOL_FONT_NAME = _get_symbolic_font_unsafe()
20
+
21
+ T = TypeVar("T")
22
+ GenericLocation = T | tuple[T] | None
23
+ XYLocation = tuple[int, int]
24
+
25
+ class BaseEnvironment:
26
+ """ Base Environment
27
+ Adapted from AIMI code
28
+ """
29
+ def __repr__(self) -> str:
30
+ """ Base string representation """
31
+ return self.__class__.__name__
32
+
33
+ @property
34
+ def is_done(self) -> bool:
35
+ """ Property indicating completion of simulation, needs overriding
36
+ """
37
+ raise NotImplementedError
38
+
39
+ def step(self) -> None:
40
+ """ Executes incremental step in discrete environment, needs overriding
41
+ """
42
+ raise NotImplementedError
43
+
44
+ def run(self, steps:int = 100, pause_for_user:bool = True) -> None:
45
+ """ Executes the simulation until either environment is done, or total
46
+ steps exceeds limit
47
+
48
+ :param steps: max. # of iterations in simulation, default 100
49
+ :param pause_for_user: require user input before beginning, default True
50
+ """
51
+ if pause_for_user:
52
+ input("Press enter to start simulation")
53
+
54
+ print(f"{self}: Running for {steps} iterations.")
55
+
56
+ for i in range(steps):
57
+ if self.is_done: # print terination message and exit
58
+ print(f"{self}: Simulation complete after {i} of {steps} iterations.")
59
+ return
60
+ self.step() # else iterate one step
61
+ print( # if loop completes, print max steps reached
62
+ f"{self}: Simulation complete after {steps} of {steps} iterations.")
63
+
64
+
65
+ class Environment(BaseEnvironment):
66
+ """ An Environment is a BaseEnvironment that has Things and Agents
67
+ """
68
+ def __init__(self):
69
+ """ Initialises set of things and agents """
70
+ self.things = set() # all things in environment
71
+ self.agents = set() # all agents in environment
72
+
73
+ @property
74
+ def is_done(self) -> bool:
75
+ """ Is considered done if there are no agents in environment """
76
+ return len(self.agents) == 0 # cannot simulate with no agents
77
+
78
+
79
+ def step(self) -> None:
80
+ """ Executes percept, program and action step for each agent. """
81
+ if not self.is_done:
82
+ actions = {
83
+ agent: agent.program(self.percept(agent))
84
+ for agent in self.agents} # dictionary of actions by agents
85
+
86
+ for agent, action in actions.items():
87
+ # iterate through each action and allow agent to execute it
88
+ self.execute_action(agent, action)
89
+
90
+ if self.is_done:
91
+ print(f"{self}: Task environment complete. No further actions.")
92
+
93
+
94
+ def percept(self, agent:Agent) -> Collection[Thing]:
95
+ """ Returns the collection, e.g. set or list, of Things that an Agent
96
+ can perceive in the current state.
97
+
98
+ :param agent: the agent that is percieving
99
+ """
100
+ raise NotImplementedError
101
+
102
+
103
+ def execute_action(self, agent:Agent, action:Action) -> None:
104
+ """ For a given agent and action, performs execution
105
+
106
+ :param agent: the agent that is executing an action
107
+ :param action: the action statement to be executed by the agent
108
+ """
109
+ raise NotImplementedError
110
+
111
+
112
+ def add_thing(self, thing:Thing, location:GenericLocation = None) -> None:
113
+ """ Adds a thing to the environment at a given location (if applicable).
114
+
115
+ :param thing: the Thing (or Thing subclass) to be added
116
+ :param location: the location to add the Thing at (if applicable)
117
+ """
118
+ if isinstance(thing, type): # if a class is given, instantiate it
119
+ if issubclass(thing, Thing):
120
+ thing = thing()
121
+ else: # not a Thing
122
+ print(f"{self}: Tried to add {thing} but its not a Thing.")
123
+ return
124
+
125
+ if not isinstance(thing, Thing): # not a Thing
126
+ print(f"{self}: Tried to add {thing} but its not a Thing.")
127
+ return
128
+
129
+ if thing in self.things: # prevent duplicate addition
130
+ print(f"{self}: Tried and failed to add duplicate {thing}.")
131
+ return
132
+
133
+ if location: # if a location is specified, set it
134
+ thing.location = location # set location of thing
135
+ print(f"{self}: Adding {thing} at {location}")
136
+ self.things.add(thing) # add thing to environment
137
+
138
+ if isinstance(thing, Agent): # if thing is an agent, add to agents list
139
+ print(f"{self}: Adding {thing} to list of agents.")
140
+ self.agents.add(thing)
141
+
142
+
143
+ def add_agent(self, agent:Agent, location:GenericLocation=None) -> None:
144
+ """ Adds an agent to the environment at a given location
145
+ (if applicable).
146
+
147
+ Wrapper for add_thing, specifically for agents.
148
+
149
+ :param agent: the Agent (or Agent subclass) to be added
150
+ :param location: the location to add the Agent at (if applicable)
151
+ """
152
+ if not isinstance(agent, Agent): # not an Agent
153
+ print(f"{self}: {agent} is not an Agent. Adding as Thing instead.")
154
+ self.add_thing(agent, location) # delegate to add_thing
155
+
156
+
157
+ def delete_thing(self, thing:Thing) -> None:
158
+ """ Removes a thing from the environment.
159
+
160
+ :param thing: the Thing (or Thing subclass) to be removed
161
+ """
162
+ if thing not in self.things: return # thing not in environment
163
+
164
+ self.things.remove(thing) # remove thing from environment
165
+ if isinstance(thing, Agent): # if thing is an agent,
166
+ self.agents.remove(thing) # remove from agents list too
167
+
168
+
169
+ def things_at(self, location:GenericLocation) -> list[Thing]:
170
+ """ Returns list of things at a given location.
171
+
172
+ :param location: the location to get Things at
173
+ """
174
+ return [thing for thing in self.things if thing.location == location]
175
+
176
+
177
+ def __call__(self, location:GenericLocation) -> list[Thing]:
178
+ """ Allows environment to be called as function to get things at location.
179
+
180
+ Wrapper for things_at.
181
+
182
+ :param location: the location to get Things at
183
+ """
184
+ return self.things_at(location)
185
+
186
+
187
+ class XYEnvironment(Environment):
188
+ """ A 2D grid environment for agents and things """
189
+ DEFAULT_BG = COLOR_BLACK # default background color
190
+
191
+ def __init__(self, width:int = 10, height:int = 10) -> None:
192
+ """ Initialises environment of given width and height
193
+
194
+ :param width: width of environment grid
195
+ :param height: height of environment grid
196
+ """
197
+ if not isinstance(width, int) or not isinstance(height, int):
198
+ raise TypeError(f"{self}: dimensions must be integers")
199
+
200
+ if width < 1 or height < 1:
201
+ raise ValueError(f"{self}: dimensions must be greater than zero")
202
+
203
+ super().__init__() # initialise base environment
204
+
205
+ if not hasattr(self, 'color'): # set default color if not overridden
206
+ self.color = self.DEFAULT_BG
207
+
208
+ self.width, self.height = width, height # set dimensions
209
+ self.observers = [] # observers of environment changes
210
+ self.bumped = set() # agents that have bumped into obstacles
211
+ self.x_start_offset = self.x_end_offset = 0 # offsets for walls
212
+ self.y_start_offset = self.y_end_offset = 0 # offsets for walls
213
+
214
+
215
+ @property
216
+ def x_start(self) -> int:
217
+ """ Starting x coordinate considering walls """
218
+ return 0 + self.x_start_offset
219
+
220
+
221
+ @property
222
+ def x_end(self) -> int:
223
+ """ Ending x coordinate considering walls """
224
+ return self.width - self.x_end_offset
225
+
226
+
227
+ @property
228
+ def y_start(self) -> int:
229
+ """ Starting y coordinate considering walls """
230
+ return 0 + self.y_start_offset
231
+
232
+
233
+ @property
234
+ def y_end(self) -> int:
235
+ """ Ending y coordinate considering walls """
236
+ return self.height - self.y_end_offset
237
+
238
+
239
+ def things_near(self, location:XYLocation, radius:int|float = 1) -> Collection[Thing]:
240
+ """ Returns list of things near a given location within a certain radius.
241
+
242
+ :param location: the location to get Things near
243
+ :param radius: the radius around location to search
244
+ """
245
+ raise NotImplementedError
246
+
247
+
248
+ def _add_wall(self, location:XYLocation) -> None:
249
+ """ Adds a wall at the given location if no obstacle is present.
250
+
251
+ :param location: the location to add wall at
252
+ """
253
+ class Wall(Obstacle): pass # simple wall class
254
+
255
+ if all(not isinstance(obj, Obstacle) # only add wall if no obstacle
256
+ for obj in self.things_at(location)):
257
+ self.add_thing(Wall(), location) # add wall to environment
258
+
259
+
260
+ def add_walls(self) -> None:
261
+ """ Adds walls around the perimeter of the environment """
262
+ if self.width > 2: # add left and right walls
263
+ for y in range(self.height):
264
+ self._add_wall((0, y))
265
+ if self.width > 1: self._add_wall((self.width-1, y))
266
+
267
+ if self.height > 2: # add top and bottom walls
268
+ for x in range(self.width):
269
+ self._add_wall((x, 0))
270
+ if self.height > 1: self._add_wall((x, self.height - 1))
271
+ self.y_start_offset += 1 # account for walls
272
+ self.y_end_offset += 1 # account for walls
273
+
274
+ if self.width > 2:
275
+ self.x_start_offset += 1 # account for walls
276
+ self.x_end_offset += 1 # account for walls
277
+
278
+
279
+ def is_valid(self, location:XYLocation) -> bool:
280
+ """ Checks if a location is valid in the environment
281
+
282
+ :param location: the location to validate
283
+ """
284
+ if not isinstance(location, Iterable): return False # must be iterable
285
+ if len(location) != 2: return False # must be length 2
286
+ if any(map(lambda x: not isinstance(x,int), location)): return False # must be ints
287
+ return True
288
+
289
+ def is_inbounds(self, location):
290
+ if not self.is_valid(location): return False
291
+ x,y = location
292
+ if not (x >= self.x_start and x < self.x_end): return False
293
+ if not (y >= self.y_start and y < self.y_end): return False
294
+ return True
295
+
296
+ @override
297
+ def add_thing(self, thing:Thing, location:XYLocation) -> None:
298
+ if location is None: # default to starting location
299
+ location = (self.x_start, self.y_start)
300
+ elif self.is_inbounds(location): # check location validity
301
+ location = tuple(location) # force tuple to make hashable
302
+ else: # invalid location
303
+ print(f"Tried and failed to add {thing} to environment")
304
+ return
305
+
306
+ if isinstance(self, Agent): # if thing is an agent
307
+ thing.bump = False # give capacity to be bumped
308
+ super().add_thing(thing, location) # delegate to base method
309
+
310
+ assert thing in self.things_at(location) # sanity check
311
+
312
+
313
+ def add_thing_randomly(self, thing:Thing):
314
+ """ Adds a thing to a random location in the environment
315
+
316
+ :param thing: the Thing (or Thing subclass) to be added
317
+ """
318
+ x = random.randint(self.x_start, self.x_end-1) # random x coord
319
+ y = random.randint(self.y_start, self.y_end-1) # random y coord
320
+ self.add_thing(thing, (x,y)) # delegate to add_thing
321
+
322
+
323
+ class GraphicEnvironment(XYEnvironment):
324
+ """ A graphical 2D grid environment for agents and things """
325
+ @override
326
+ def run(self, graphical=True, steps=100, **kwargs) -> None:
327
+ """ Executes the simulation in graphical or base mode.
328
+
329
+ :param graphical: whether to run graphical version
330
+ :param steps: max. # of iterations in simulation, default 100
331
+ :param **kwargs: additional arguments for environment base class
332
+ """
333
+ if graphical: # run graphical version
334
+ EnvironmentApp(self, steps=steps, name=f"{self}", **kwargs).run()
335
+ else: # run base version
336
+ super().run(steps=steps, **kwargs)
337
+
338
+
339
+ class EnvironmentApp(App):
340
+ """EnvironmentApp
341
+ Graphical version of an XYEnvironment
342
+ """
343
+ # size = width, height = 600, 400 # uncomment to override
344
+ def __init__(self,
345
+ environment:XYEnvironment|None = None,
346
+ steps:int = 100,
347
+ **kwargs) -> None:
348
+ """ Initialises graphical environment app
349
+
350
+ :param environment: the XYEnvironment to visualise
351
+ :param steps: max. # of iterations in simulation, default 100
352
+ :param **kwargs: additional arguments for App base class
353
+ """
354
+ if environment is None: # default to 12x8 XYEnvironment
355
+ print(f"{self}: No environment specified, using default")
356
+ environment = XYEnvironment(12, 8)
357
+ if not isinstance(environment, XYEnvironment): # check type
358
+ raise TypeError(f"{self}: environment must be XYEnvironment")
359
+
360
+ self.environment = environment
361
+ self._fit_to_environment() # fit window aspect to environment
362
+ super().__init__(**kwargs) # initialise base App class
363
+
364
+ # font for rendering things
365
+ self.thing_font = pygame.font.SysFont(SYMBOL_FONT_NAME, 28)
366
+
367
+ self.counter = -1 # start at -1 to allow initial render
368
+ self.steps = steps # max. steps in simulation
369
+ self._flag = True # flag for completion message
370
+
371
+
372
+ def _fit_to_environment(self) -> None:
373
+ """ Fit width and height ratios to match environment """
374
+ # determine whether environment is wider than tall
375
+ _wider_than_tall = self.environment.width > self.environment.height
376
+
377
+ if _wider_than_tall:
378
+ xy_ratio = self.environment.width/self.environment.height
379
+ a, b = self.height, self.width
380
+ else:
381
+ xy_ratio = self.environment.height/self.environment.width
382
+ b, a = self.height, self.width
383
+
384
+ a = b // xy_ratio # adjust dimensions
385
+ b = int(a*xy_ratio)
386
+
387
+ self.height, self.width = (a,b) if _wider_than_tall else (b,a)
388
+ self.size = self.width, self.height # set size attribute
389
+
390
+
391
+ @override
392
+ def update(self) -> None:
393
+ """ Main process loop
394
+ Steps the environment simulation
395
+ """
396
+ if self.counter < 0: # initial render only
397
+ self.counter += 1
398
+ return
399
+ if self.counter < self.steps and not self.environment.is_done:
400
+ self.environment.step()
401
+ self.counter += 1
402
+ elif self._flag: # print completion message once
403
+ print(f"{self.environment}: Simulation complete after {self.counter} of {self.steps} iterations.")
404
+ self._flag = False
405
+
406
+
407
+ @override
408
+ def render(self) -> None:
409
+ """ Main render loop
410
+ Renders the environment grid and things
411
+ """
412
+ self.screen.fill(self.environment.color) # write background color
413
+ self.render_grid() # render grid
414
+ self.render_things() # render things
415
+
416
+
417
+ def render_grid(self) -> None:
418
+ """ Render environment grid """
419
+ nx,ny = self.environment.width, self.environment.height
420
+ self.tile_size = self.width // nx # size of each tile
421
+ tile_origin = (0, 0)
422
+
423
+ tiles:list[list[pygame.Rect]] = [] # 2D list of tile rects
424
+
425
+ for i in range(ny): # for each row
426
+ row = []
427
+ for j in range(nx): # for each column
428
+ tileRect = pygame.Rect( # define tile rectangle
429
+ tile_origin[0] + j * self.tile_size,
430
+ tile_origin[1] + i * self.tile_size,
431
+ self.tile_size, self.tile_size)
432
+ # draw tile border
433
+ pygame.draw.rect(self.screen, COLOR_WHITE, tileRect, 1)
434
+ # add tile render object to row
435
+ row.append(tileRect)
436
+ tiles.append(row) # add row to tiles
437
+
438
+ self.tiles = tiles # store tiles for later use
439
+
440
+
441
+ def render_things(self) -> None:
442
+ """ Render things in environment """
443
+ locations:dict = {} # map of locations to things
444
+
445
+ # build location map
446
+ for thing in self.environment.things:
447
+ if thing.location in locations:
448
+ locations[thing.location].append(thing)
449
+ else:
450
+ locations[thing.location] = [thing]
451
+
452
+ # render things at each location
453
+ for location, things in locations.items():
454
+ n_things = len(things)
455
+ if n_things > 1: # multiple things, pick one at random per render
456
+ thing = things[random.randint(0,n_things-1)]
457
+ elif n_things == 1: # single thing
458
+ thing = things[0]
459
+ else: # no things to render at this location
460
+ continue
461
+ # determine render location
462
+ renderLoc = self.tiles[location[1]][location[0]].center
463
+ thingRender = self.thing_font.render(str(thing), True, COLOR_WHITE)
464
+ thingRect = thingRender.get_rect()
465
+ thingRect.center = renderLoc
466
+ # blit thing to screen
467
+ self.screen.blit(thingRender, thingRect)
@@ -0,0 +1,92 @@
1
+ """THINGS.PY
2
+
3
+ Contains class definitions for some things
4
+ """
5
+ from collections.abc import Callable, Collection, Iterable
6
+ from typing import override, Union
7
+
8
+ from ..util.fonts import platform
9
+
10
+ Action = Union[str, Iterable[str]]
11
+
12
+ class Thing:
13
+ """The base class for all things"""
14
+ @override
15
+ def __repr__(self) -> str:
16
+ return "❓" if platform != "darwin" else "?"
17
+
18
+ ## Some physical things
19
+
20
+ class Obstacle(Thing):
21
+ @override
22
+ def __repr__(self) -> str:
23
+ return "🚧" if platform != "darwin" else "X"
24
+
25
+
26
+ class Food(Thing):
27
+ @override
28
+ def __repr__(self) -> str:
29
+ return "🍔" if platform != "darwin" else "f"
30
+
31
+
32
+ class Water(Thing):
33
+ @override
34
+ def __repr__(self) -> str:
35
+ return "💧" if platform != "darwin" else "w"
36
+
37
+
38
+ class Animal(Thing):
39
+ """ A generic animal thing """
40
+ pass
41
+
42
+
43
+ class Dog(Animal):
44
+ """If it looks like a dog and it barks like a dog ..."""
45
+ @override
46
+ def __repr__(self) -> str:
47
+ return "🐶" if platform != "darwin" else "d"
48
+
49
+
50
+ ## Some agent things
51
+
52
+ class Agent(Thing):
53
+ """ Base class for all agents """
54
+ pass
55
+
56
+
57
+ class RationalAgent(Agent):
58
+ """ Base class for rational agent """
59
+ def __init__(self, program:Callable) -> None:
60
+ self.performance = 0
61
+ # Check that program is callable function
62
+ if program is None or not isinstance(program, Callable):
63
+ raise ValueError("No valid program provided")
64
+ self.program:Callable = program
65
+
66
+
67
+ class ModelBasedAgent(RationalAgent):
68
+ """ Base class for model based agents
69
+
70
+ Requires a program method to be defined
71
+ program(percepts:Collection[Thing]) -> Action
72
+ """
73
+ def __init__(self):
74
+ """ Initialize the agent with its program """
75
+ super().__init__(self.program)
76
+
77
+ def program(self, percepts:Collection[Thing]) -> Action:
78
+ """ The agent program
79
+ Given a collection of percepts, return an action
80
+ """
81
+ raise NotImplementedError
82
+
83
+ ## State
84
+ class State(Thing):
85
+ """ Base class for states """
86
+ @override
87
+ def __repr__(self) -> str:
88
+ return self.__class__.__name__
89
+
90
+ class Bump(State):
91
+ """ A bump state """
92
+ pass