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.
- {co2114-2025.2.2 → co2114-2026.0.3}/PKG-INFO +4 -3
- co2114-2026.0.3/co2114/agent/environment.py +467 -0
- co2114-2026.0.3/co2114/agent/things.py +92 -0
- {co2114-2025.2.2 → co2114-2026.0.3}/co2114/engine.py +114 -37
- co2114-2026.0.3/co2114/search/graph.py +445 -0
- co2114-2026.0.3/co2114/search/things.py +56 -0
- co2114-2026.0.3/co2114/util/__init__.py +53 -0
- {co2114-2025.2.2 → co2114-2026.0.3}/co2114.egg-info/PKG-INFO +4 -3
- {co2114-2025.2.2 → co2114-2026.0.3}/setup.py +2 -2
- co2114-2025.2.2/co2114/agent/environment.py +0 -301
- co2114-2025.2.2/co2114/agent/things.py +0 -68
- co2114-2025.2.2/co2114/search/graph.py +0 -296
- co2114-2025.2.2/co2114/search/things.py +0 -32
- co2114-2025.2.2/co2114/util/__init__.py +0 -29
- {co2114-2025.2.2 → co2114-2026.0.3}/LICENSE +0 -0
- {co2114-2025.2.2 → co2114-2026.0.3}/README.md +0 -0
- {co2114-2025.2.2 → co2114-2026.0.3}/co2114/__init__.py +0 -0
- {co2114-2025.2.2 → co2114-2026.0.3}/co2114/agent/__init__.py +0 -0
- {co2114-2025.2.2 → co2114-2026.0.3}/co2114/constraints/__init__.py +0 -0
- {co2114-2025.2.2 → co2114-2026.0.3}/co2114/constraints/csp/__init__.py +0 -0
- {co2114-2025.2.2 → co2114-2026.0.3}/co2114/constraints/csp/util.py +0 -0
- {co2114-2025.2.2 → co2114-2026.0.3}/co2114/constraints/magic.py +0 -0
- {co2114-2025.2.2 → co2114-2026.0.3}/co2114/constraints/sudoku.py +0 -0
- {co2114-2025.2.2 → co2114-2026.0.3}/co2114/optimisation/__init__.py +0 -0
- {co2114-2025.2.2 → co2114-2026.0.3}/co2114/optimisation/minimax.py +0 -0
- {co2114-2025.2.2 → co2114-2026.0.3}/co2114/optimisation/planning.py +0 -0
- {co2114-2025.2.2 → co2114-2026.0.3}/co2114/optimisation/things.py +0 -0
- {co2114-2025.2.2 → co2114-2026.0.3}/co2114/reasoning/__init__.py +0 -0
- {co2114-2025.2.2 → co2114-2026.0.3}/co2114/reasoning/cluedo.py +0 -0
- {co2114-2025.2.2 → co2114-2026.0.3}/co2114/reasoning/inference.py +0 -0
- {co2114-2025.2.2 → co2114-2026.0.3}/co2114/reasoning/logic.py +0 -0
- {co2114-2025.2.2 → co2114-2026.0.3}/co2114/search/__init__.py +0 -0
- {co2114-2025.2.2 → co2114-2026.0.3}/co2114/search/maze.py +0 -0
- {co2114-2025.2.2 → co2114-2026.0.3}/co2114/search/util.py +0 -0
- {co2114-2025.2.2 → co2114-2026.0.3}/co2114/util/colours.py +0 -0
- {co2114-2025.2.2 → co2114-2026.0.3}/co2114/util/fonts.py +0 -0
- {co2114-2025.2.2 → co2114-2026.0.3}/co2114.egg-info/SOURCES.txt +0 -0
- {co2114-2025.2.2 → co2114-2026.0.3}/co2114.egg-info/dependency_links.txt +0 -0
- {co2114-2025.2.2 → co2114-2026.0.3}/co2114.egg-info/requires.txt +0 -0
- {co2114-2025.2.2 → co2114-2026.0.3}/co2114.egg-info/top_level.txt +0 -0
- {co2114-2025.2.2 → co2114-2026.0.3}/setup.cfg +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: co2114
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2026.0.3
|
|
4
4
|
Summary: codebase for co2114
|
|
5
5
|
Author: wil ward
|
|
6
|
-
Requires-Python: >=3.
|
|
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
|