co2114 2025.2.2__py3-none-any.whl → 2026.0.2__py3-none-any.whl
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/agent/environment.py +300 -134
- co2114/agent/things.py +34 -10
- co2114/engine.py +114 -37
- co2114/search/graph.py +278 -129
- co2114/search/things.py +38 -14
- co2114/util/__init__.py +35 -11
- {co2114-2025.2.2.dist-info → co2114-2026.0.2.dist-info}/METADATA +4 -3
- {co2114-2025.2.2.dist-info → co2114-2026.0.2.dist-info}/RECORD +11 -11
- {co2114-2025.2.2.dist-info → co2114-2026.0.2.dist-info}/WHEEL +1 -1
- {co2114-2025.2.2.dist-info → co2114-2026.0.2.dist-info/licenses}/LICENSE +0 -0
- {co2114-2025.2.2.dist-info → co2114-2026.0.2.dist-info}/top_level.txt +0 -0
co2114/agent/environment.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"""ENVIRONMENT.PY
|
|
2
|
-
This contains code for agent environments
|
|
2
|
+
This contains code for agent environments and simulations
|
|
3
3
|
"""
|
|
4
4
|
import warnings
|
|
5
5
|
import math
|
|
6
6
|
import random
|
|
7
|
-
from collections.abc import
|
|
7
|
+
from collections.abc import Collection, Iterable
|
|
8
|
+
from typing import Union, TypeVar, override
|
|
8
9
|
|
|
9
10
|
with warnings.catch_warnings():
|
|
10
11
|
warnings.simplefilter("ignore")
|
|
@@ -13,163 +14,276 @@ with warnings.catch_warnings():
|
|
|
13
14
|
from ..engine import App
|
|
14
15
|
from ..util.colours import COLOR_BLACK, COLOR_WHITE
|
|
15
16
|
from ..util.fonts import _get_symbolic_font_unsafe
|
|
16
|
-
from .things import Thing, Agent, Obstacle
|
|
17
|
+
from .things import Thing, Agent, Obstacle, Action
|
|
17
18
|
|
|
18
19
|
SYMBOL_FONT_NAME = _get_symbolic_font_unsafe()
|
|
19
20
|
|
|
21
|
+
T = TypeVar("T")
|
|
22
|
+
GenericLocation = T | tuple[T] | None
|
|
23
|
+
XYLocation = tuple[int, int]
|
|
24
|
+
|
|
20
25
|
class BaseEnvironment:
|
|
21
26
|
""" Base Environment
|
|
22
27
|
Adapted from AIMI code
|
|
23
28
|
"""
|
|
24
|
-
def __repr__(self):
|
|
29
|
+
def __repr__(self) -> str:
|
|
30
|
+
""" Base string representation """
|
|
25
31
|
return self.__class__.__name__
|
|
32
|
+
|
|
26
33
|
@property
|
|
27
|
-
def is_done(self):
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
"""
|
|
32
51
|
if pause_for_user:
|
|
33
52
|
input("Press enter to start simulation")
|
|
53
|
+
|
|
34
54
|
print(f"{self}: Running for {steps} iterations.")
|
|
55
|
+
|
|
35
56
|
for i in range(steps):
|
|
36
|
-
if self.is_done:
|
|
57
|
+
if self.is_done: # print terination message and exit
|
|
37
58
|
print(f"{self}: Simulation complete after {i} of {steps} iterations.")
|
|
38
59
|
return
|
|
39
|
-
self.step()
|
|
40
|
-
print(
|
|
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
|
+
|
|
41
64
|
|
|
42
65
|
class Environment(BaseEnvironment):
|
|
43
|
-
""" Environment
|
|
44
|
-
An Environment is a BaseEnvironment that has Things and Agents
|
|
66
|
+
""" An Environment is a BaseEnvironment that has Things and Agents
|
|
45
67
|
"""
|
|
46
68
|
def __init__(self):
|
|
47
|
-
|
|
48
|
-
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
|
+
|
|
49
73
|
@property
|
|
50
|
-
def is_done(self):
|
|
74
|
+
def is_done(self) -> bool:
|
|
75
|
+
""" Is considered done if there are no agents in environment """
|
|
51
76
|
return len(self.agents) == 0 # cannot simulate with no agents
|
|
52
|
-
|
|
53
|
-
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def step(self) -> None:
|
|
80
|
+
""" Executes percept, program and action step for each agent. """
|
|
54
81
|
if not self.is_done:
|
|
55
|
-
actions = {
|
|
56
|
-
|
|
82
|
+
actions = {
|
|
83
|
+
agent: agent.program(self.percept(agent))
|
|
84
|
+
for agent in self.agents} # dictionary of actions by agents
|
|
57
85
|
|
|
58
86
|
for agent, action in actions.items():
|
|
59
|
-
|
|
87
|
+
# iterate through each action and allow agent to execute it
|
|
88
|
+
self.execute_action(agent, action)
|
|
89
|
+
|
|
60
90
|
if self.is_done:
|
|
61
91
|
print(f"{self}: Task environment complete. No further actions.")
|
|
62
92
|
|
|
63
|
-
def percept(self, agent:Agent):
|
|
64
|
-
NotImplemented
|
|
65
|
-
def execute_action(self, agent:Agent, action):
|
|
66
|
-
NotImplemented
|
|
67
93
|
|
|
68
|
-
def
|
|
69
|
-
|
|
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
|
|
70
119
|
if issubclass(thing, Thing):
|
|
71
120
|
thing = thing()
|
|
72
|
-
else:
|
|
121
|
+
else: # not a Thing
|
|
73
122
|
print(f"{self}: Tried to add {thing} but its not a Thing.")
|
|
74
123
|
return
|
|
75
124
|
|
|
76
|
-
if not isinstance(thing, Thing):
|
|
125
|
+
if not isinstance(thing, Thing): # not a Thing
|
|
77
126
|
print(f"{self}: Tried to add {thing} but its not a Thing.")
|
|
78
127
|
return
|
|
79
128
|
|
|
80
|
-
if thing in self.things:
|
|
129
|
+
if thing in self.things: # prevent duplicate addition
|
|
81
130
|
print(f"{self}: Tried and failed to add duplicate {thing}.")
|
|
82
131
|
return
|
|
83
132
|
|
|
84
|
-
if location:
|
|
85
|
-
thing.location = location
|
|
133
|
+
if location: # if a location is specified, set it
|
|
134
|
+
thing.location = location # set location of thing
|
|
86
135
|
print(f"{self}: Adding {thing} at {location}")
|
|
87
|
-
self.things.add(thing)
|
|
136
|
+
self.things.add(thing) # add thing to environment
|
|
88
137
|
|
|
89
|
-
if isinstance(thing, Agent):
|
|
138
|
+
if isinstance(thing, Agent): # if thing is an agent, add to agents list
|
|
90
139
|
print(f"{self}: Adding {thing} to list of agents.")
|
|
91
140
|
self.agents.add(thing)
|
|
92
141
|
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
95
153
|
print(f"{self}: {agent} is not an Agent. Adding as Thing instead.")
|
|
96
|
-
self.add_thing(agent, location)
|
|
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
|
|
97
167
|
|
|
98
|
-
def delete_thing(self, thing:Thing):
|
|
99
|
-
if thing not in self.things:
|
|
100
|
-
return
|
|
101
|
-
self.things.remove(thing)
|
|
102
|
-
if isinstance(thing, Agent):
|
|
103
|
-
self.agents.remove(thing)
|
|
104
168
|
|
|
105
|
-
def things_at(self, location):
|
|
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
|
+
"""
|
|
106
174
|
return [thing for thing in self.things if thing.location == location]
|
|
107
|
-
|
|
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
|
+
"""
|
|
108
184
|
return self.things_at(location)
|
|
109
185
|
|
|
186
|
+
|
|
110
187
|
class XYEnvironment(Environment):
|
|
111
|
-
|
|
188
|
+
""" A 2D grid environment for agents and things """
|
|
189
|
+
DEFAULT_BG = COLOR_BLACK # default background color
|
|
112
190
|
|
|
113
|
-
def __init__(self, width=10, height=10):
|
|
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
|
+
"""
|
|
114
197
|
if not isinstance(width, int) or not isinstance(height, int):
|
|
115
198
|
raise TypeError(f"{self}: dimensions must be integers")
|
|
199
|
+
|
|
116
200
|
if width < 1 or height < 1:
|
|
117
201
|
raise ValueError(f"{self}: dimensions must be greater than zero")
|
|
118
|
-
|
|
119
|
-
|
|
202
|
+
|
|
203
|
+
super().__init__() # initialise base environment
|
|
204
|
+
|
|
205
|
+
if not hasattr(self, 'color'): # set default color if not overridden
|
|
120
206
|
self.color = self.DEFAULT_BG
|
|
121
|
-
|
|
122
|
-
self.
|
|
123
|
-
self.
|
|
124
|
-
self.
|
|
125
|
-
self.
|
|
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
|
|
126
213
|
|
|
214
|
+
|
|
127
215
|
@property
|
|
128
|
-
def x_start(self):
|
|
216
|
+
def x_start(self) -> int:
|
|
217
|
+
""" Starting x coordinate considering walls """
|
|
129
218
|
return 0 + self.x_start_offset
|
|
219
|
+
|
|
220
|
+
|
|
130
221
|
@property
|
|
131
|
-
def x_end(self):
|
|
222
|
+
def x_end(self) -> int:
|
|
223
|
+
""" Ending x coordinate considering walls """
|
|
132
224
|
return self.width - self.x_end_offset
|
|
225
|
+
|
|
226
|
+
|
|
133
227
|
@property
|
|
134
|
-
def y_start(self):
|
|
228
|
+
def y_start(self) -> int:
|
|
229
|
+
""" Starting y coordinate considering walls """
|
|
135
230
|
return 0 + self.y_start_offset
|
|
231
|
+
|
|
232
|
+
|
|
136
233
|
@property
|
|
137
|
-
def y_end(self):
|
|
234
|
+
def y_end(self) -> int:
|
|
235
|
+
""" Ending y coordinate considering walls """
|
|
138
236
|
return self.height - self.y_end_offset
|
|
139
237
|
|
|
140
238
|
|
|
141
|
-
def things_near(self, location, radius=1):
|
|
142
|
-
|
|
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
|
+
"""
|
|
143
245
|
raise NotImplementedError
|
|
144
246
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
149
256
|
for obj in self.things_at(location)):
|
|
150
|
-
self.add_thing(Wall(), location)
|
|
257
|
+
self.add_thing(Wall(), location) # add wall to environment
|
|
151
258
|
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
154
263
|
for y in range(self.height):
|
|
155
264
|
self._add_wall((0, y))
|
|
156
|
-
if self.width > 1:
|
|
157
|
-
|
|
158
|
-
if self.height > 2:
|
|
265
|
+
if self.width > 1: self._add_wall((self.width-1, y))
|
|
266
|
+
|
|
267
|
+
if self.height > 2: # add top and bottom walls
|
|
159
268
|
for x in range(self.width):
|
|
160
269
|
self._add_wall((x, 0))
|
|
161
|
-
if self.height > 1:
|
|
162
|
-
|
|
163
|
-
self.
|
|
164
|
-
|
|
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
|
+
|
|
165
274
|
if self.width > 2:
|
|
166
|
-
self.x_start_offset += 1
|
|
167
|
-
self.x_end_offset += 1
|
|
275
|
+
self.x_start_offset += 1 # account for walls
|
|
276
|
+
self.x_end_offset += 1 # account for walls
|
|
168
277
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if
|
|
172
|
-
|
|
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
|
|
173
287
|
return True
|
|
174
288
|
|
|
175
289
|
def is_inbounds(self, location):
|
|
@@ -179,123 +293,175 @@ class XYEnvironment(Environment):
|
|
|
179
293
|
if not (y >= self.y_start and y < self.y_end): return False
|
|
180
294
|
return True
|
|
181
295
|
|
|
182
|
-
|
|
183
|
-
|
|
296
|
+
@override
|
|
297
|
+
def add_thing(self, thing:Thing, location:XYLocation) -> None:
|
|
298
|
+
if location is None: # default to starting location
|
|
184
299
|
location = (self.x_start, self.y_start)
|
|
185
|
-
elif self.is_inbounds(location):
|
|
300
|
+
elif self.is_inbounds(location): # check location validity
|
|
186
301
|
location = tuple(location) # force tuple to make hashable
|
|
187
|
-
else:
|
|
302
|
+
else: # invalid location
|
|
188
303
|
print(f"Tried and failed to add {thing} to environment")
|
|
189
304
|
return
|
|
190
305
|
|
|
191
|
-
if isinstance(self, Agent):
|
|
306
|
+
if isinstance(self, Agent): # if thing is an agent
|
|
192
307
|
thing.bump = False # give capacity to be bumped
|
|
193
|
-
super().add_thing(thing, location)
|
|
308
|
+
super().add_thing(thing, location) # delegate to base method
|
|
309
|
+
|
|
310
|
+
assert thing in self.things_at(location) # sanity check
|
|
194
311
|
|
|
195
|
-
assert thing in self.things_at(location)
|
|
196
312
|
|
|
197
313
|
def add_thing_randomly(self, thing:Thing):
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
+
|
|
201
322
|
|
|
202
323
|
class GraphicEnvironment(XYEnvironment):
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|
205
334
|
EnvironmentApp(self, steps=steps, name=f"{self}", **kwargs).run()
|
|
206
|
-
else:
|
|
335
|
+
else: # run base version
|
|
207
336
|
super().run(steps=steps, **kwargs)
|
|
208
337
|
|
|
338
|
+
|
|
209
339
|
class EnvironmentApp(App):
|
|
210
340
|
"""EnvironmentApp
|
|
211
|
-
Graphical version of
|
|
341
|
+
Graphical version of an XYEnvironment
|
|
212
342
|
"""
|
|
213
343
|
# size = width, height = 600, 400 # uncomment to override
|
|
214
|
-
def __init__(self,
|
|
215
|
-
|
|
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
|
|
216
355
|
print(f"{self}: No environment specified, using default")
|
|
217
356
|
environment = XYEnvironment(12, 8)
|
|
218
|
-
if not isinstance(environment, XYEnvironment):
|
|
357
|
+
if not isinstance(environment, XYEnvironment): # check type
|
|
219
358
|
raise TypeError(f"{self}: environment must be XYEnvironment")
|
|
220
359
|
|
|
221
360
|
self.environment = environment
|
|
222
|
-
self._fit_to_environment()
|
|
223
|
-
super().__init__(**kwargs)
|
|
361
|
+
self._fit_to_environment() # fit window aspect to environment
|
|
362
|
+
super().__init__(**kwargs) # initialise base App class
|
|
224
363
|
|
|
364
|
+
# font for rendering things
|
|
225
365
|
self.thing_font = pygame.font.SysFont(SYMBOL_FONT_NAME, 28)
|
|
226
|
-
|
|
227
|
-
self.
|
|
228
|
-
self.
|
|
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
|
|
229
370
|
|
|
230
|
-
|
|
371
|
+
|
|
372
|
+
def _fit_to_environment(self) -> None:
|
|
231
373
|
""" Fit width and height ratios to match environment """
|
|
232
|
-
|
|
233
|
-
|
|
374
|
+
# determine whether environment is wider than tall
|
|
375
|
+
_wider_than_tall = self.environment.width > self.environment.height
|
|
376
|
+
|
|
377
|
+
if _wider_than_tall:
|
|
234
378
|
xy_ratio = self.environment.width/self.environment.height
|
|
235
379
|
a, b = self.height, self.width
|
|
236
380
|
else:
|
|
237
381
|
xy_ratio = self.environment.height/self.environment.width
|
|
238
382
|
b, a = self.height, self.width
|
|
239
383
|
|
|
240
|
-
a = b // xy_ratio
|
|
384
|
+
a = b // xy_ratio # adjust dimensions
|
|
241
385
|
b = int(a*xy_ratio)
|
|
242
386
|
|
|
243
|
-
self.height, self.width = (a,b) if
|
|
244
|
-
self.size = self.width, self.height
|
|
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
|
+
|
|
245
390
|
|
|
246
|
-
|
|
247
|
-
|
|
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
|
|
248
397
|
self.counter += 1
|
|
249
398
|
return
|
|
250
399
|
if self.counter < self.steps and not self.environment.is_done:
|
|
251
400
|
self.environment.step()
|
|
252
401
|
self.counter += 1
|
|
253
|
-
elif self._flag:
|
|
402
|
+
elif self._flag: # print completion message once
|
|
254
403
|
print(f"{self.environment}: Simulation complete after {self.counter} of {self.steps} iterations.")
|
|
255
404
|
self._flag = False
|
|
256
|
-
|
|
257
|
-
def render(self):
|
|
258
|
-
self.screen.fill(self.environment.color)
|
|
259
|
-
self.render_grid()
|
|
260
|
-
self.render_things()
|
|
261
405
|
|
|
262
|
-
|
|
263
|
-
|
|
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 """
|
|
264
419
|
nx,ny = self.environment.width, self.environment.height
|
|
265
|
-
self.tile_size = self.width // nx
|
|
420
|
+
self.tile_size = self.width // nx # size of each tile
|
|
266
421
|
tile_origin = (0, 0)
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
|
272
429
|
tile_origin[0] + j * self.tile_size,
|
|
273
430
|
tile_origin[1] + i * self.tile_size,
|
|
274
431
|
self.tile_size, self.tile_size)
|
|
432
|
+
# draw tile border
|
|
275
433
|
pygame.draw.rect(self.screen, COLOR_WHITE, tileRect, 1)
|
|
276
|
-
|
|
434
|
+
# add tile render object to row
|
|
277
435
|
row.append(tileRect)
|
|
278
|
-
tiles.append(row)
|
|
279
|
-
self.tiles = tiles
|
|
436
|
+
tiles.append(row) # add row to tiles
|
|
280
437
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
|
284
446
|
for thing in self.environment.things:
|
|
285
447
|
if thing.location in locations:
|
|
286
448
|
locations[thing.location].append(thing)
|
|
287
449
|
else:
|
|
288
450
|
locations[thing.location] = [thing]
|
|
451
|
+
|
|
452
|
+
# render things at each location
|
|
289
453
|
for location, things in locations.items():
|
|
290
454
|
n_things = len(things)
|
|
291
|
-
if n_things > 1:
|
|
455
|
+
if n_things > 1: # multiple things, pick one at random per render
|
|
292
456
|
thing = things[random.randint(0,n_things-1)]
|
|
293
|
-
elif n_things == 1:
|
|
457
|
+
elif n_things == 1: # single thing
|
|
294
458
|
thing = things[0]
|
|
295
|
-
else:
|
|
459
|
+
else: # no things to render at this location
|
|
296
460
|
continue
|
|
461
|
+
# determine render location
|
|
297
462
|
renderLoc = self.tiles[location[1]][location[0]].center
|
|
298
463
|
thingRender = self.thing_font.render(str(thing), True, COLOR_WHITE)
|
|
299
464
|
thingRect = thingRender.get_rect()
|
|
300
465
|
thingRect.center = renderLoc
|
|
466
|
+
# blit thing to screen
|
|
301
467
|
self.screen.blit(thingRender, thingRect)
|