maeves-ecosphere 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: maeves-ecosphere
3
+ Version: 0.1.2
4
+ Summary: A tile-based procedural ecosystem simulation, with an object component system.
5
+ Author: MolassesLover
@@ -0,0 +1,108 @@
1
+ # Maeve's Ecosphere
2
+
3
+ <p><a href="https://ko-fi.com/molasses"><img src="https://img.shields.io/badge/donate-kofi-blue?style=for-the-badge&amp;logo=ko-fi&amp;color=e57578&amp;logoColor=FFFFFF&amp;labelColor=262a35" alt="Ko-Fi"></a>
4
+ <a href="https://www.patreon.com/molasseslover"><img src="https://img.shields.io/badge/donate-patreon-blue?style=for-the-badge&amp;logo=patreon&amp;color=e57578&amp;logoColor=FFFFFF&amp;labelColor=262a35" alt="Patreon"></a>
5
+
6
+ This is a simple procedural cellular automata simulation resembling an ecosystem.
7
+ I made this, in my boredom, to toy around with during a lengthy
8
+ [PEM](https://en.wikipedia.org/wiki/Post-exertional_malaise) flare-up.
9
+
10
+ The project makes use of rudimentary ECS-inspired framework, which makes use
11
+ of Python's dictionaries to manage data. Every tile (cell) is an object which
12
+ may contain another object as its `occupant`. These objects are referred to
13
+ as `SimulationObject`s and are this project's (much more bloated) equivalent
14
+ to entities. Every `SimulationObject` contains a list of components, stored as
15
+ dictionaries. Every component dictionary contains a callback function, which
16
+ serves as a system instance, as you'd see in a project using a conventional
17
+ ECS architecture. Lastly, `SimulationObject`s also contain a `component_data`
18
+ dictionary which stores data accessed by components, straying from real
19
+ ECS implementations where data is stored directly inside of components.
20
+
21
+ ## Preview
22
+
23
+ Below is a screenshot of the simulation after about 1h30m:
24
+
25
+ ![A screenshot of the Maeve's Ecosphere project depicting various tiles, each one depicting either a tree, grass, mushroom, or an isopod.](/docs/img/screenshot.png)
26
+
27
+ Below is a brief glimpse at the early stage of a simulation:
28
+
29
+ ![A looping GIF video depicting the ecosphere, with various isopods foraging, and some vegetation growing.](/docs/img/preview.gif)
30
+
31
+ If you'd like a more extensive preview, take a look at this
32
+ [YouTube video](https://youtu.be/FK47rwov3aU).
33
+
34
+ ## Features
35
+
36
+ - Tiles which may be occupied by simulated objects
37
+ - Simulated objects which may:
38
+ - Forage to other tiles.
39
+ - Consume other objects.
40
+ - Starve to death if too hungry.
41
+ - Produce objects in neighbouring tiles by:
42
+ - Reproducing with partner objects.
43
+ - Propogating themselves on their own.
44
+ - Age into different life stages.
45
+ - Die after reaching a certain age.
46
+ - Decompose after death.
47
+ - Affect the nutrition of a tile on birth or death.
48
+
49
+ ### Organisms
50
+ #### Trees
51
+ Trees take a significant amount of time to age into adulthood, and
52
+ consequently encroach on surrounding organisms over time.
53
+
54
+ #### Grass
55
+ Grass has a very short lifespan, and serves as the initial food source for
56
+ animals.
57
+
58
+ #### Mushrooms
59
+ Mushrooms take a long time to reach adulthood, spread prior to adulthood, and
60
+ serve as the main food source for animals.
61
+
62
+ #### Isopods
63
+ Isopods forage the map, eating and mating with partner isopods when they can.
64
+
65
+ ## Cloning
66
+ This project makes use of the [Git LFS](https://git-lfs.com/) extension, please
67
+ make sure to install it prior to cloning.
68
+
69
+ ```sh
70
+ ➜ git lfs install
71
+ ```
72
+
73
+ Already cloned the repository before setting Git LFS up? After you've
74
+ installed Git LFS, these commands should download all Git LFS files.
75
+
76
+ ```sh
77
+ ➜ git lfs fetch --all
78
+ ➜ git lfs pull
79
+ ➜ git lfs checkout
80
+ ```
81
+
82
+ ## Credits
83
+ Image assets under the [`res/img/`](res/img) directory are taken from or
84
+ derivations of Pierre Vandermaesen' 1-bit asset pack,
85
+ found [here](https://pierre-vandermaesen.itch.io/1-bit-16x16-asset-packs).
86
+
87
+ ## Running & Installing
88
+ ### Nix
89
+
90
+ The recommended installation route is with use of Nix. This project provides a Nix
91
+ flake which can be installed via Home Manager, your NixOS configuration, and
92
+ a plethora of other solutions which support Nix flakes.
93
+
94
+ Alternatively the project can simply be run without installation using the following command:
95
+
96
+ ```sh
97
+ ➜ nix build .; ./result/result/bin/maeves-ecosphere
98
+ ```
99
+
100
+ ### Python
101
+
102
+ Installation is required to run this project without Nix.
103
+
104
+ ```sh
105
+ ➜ pip install pygame cython
106
+ ➜ pip install .
107
+ ➜ python -m mecosphere.cli
108
+ ```
@@ -0,0 +1,24 @@
1
+ [project]
2
+ name = "maeves-ecosphere"
3
+ version = "0.1.2"
4
+ description = "A tile-based procedural ecosystem simulation, with an object component system."
5
+
6
+ authors = [
7
+ {name = "MolassesLover"}
8
+ ]
9
+
10
+ [project.scripts]
11
+ maeves-ecosphere = "mecosphere:cli.main"
12
+
13
+ [build-system]
14
+ requires = ["setuptools", "cython"]
15
+
16
+ [tool.setuptools]
17
+ ext-modules = [
18
+ { name = "mecosphere", sources = [
19
+ "src/mecosphere/application.py",
20
+ "src/mecosphere/components.py",
21
+ "src/mecosphere/simulation.py",
22
+ "src/mecosphere/tilemap.py"
23
+ ] }
24
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: maeves-ecosphere
3
+ Version: 0.1.2
4
+ Summary: A tile-based procedural ecosystem simulation, with an object component system.
5
+ Author: MolassesLover
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/maeves_ecosphere.egg-info/PKG-INFO
4
+ src/maeves_ecosphere.egg-info/SOURCES.txt
5
+ src/maeves_ecosphere.egg-info/dependency_links.txt
6
+ src/maeves_ecosphere.egg-info/entry_points.txt
7
+ src/maeves_ecosphere.egg-info/top_level.txt
8
+ src/mecosphere/__init__.py
9
+ src/mecosphere/application.py
10
+ src/mecosphere/cli.py
11
+ src/mecosphere/components.py
12
+ src/mecosphere/simulation.py
13
+ src/mecosphere/tilemap.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ maeves-ecosphere = mecosphere:cli.main
@@ -0,0 +1 @@
1
+ from . import *
@@ -0,0 +1,356 @@
1
+ import argparse
2
+ import sys
3
+ import random
4
+ import time
5
+
6
+ import pygame
7
+
8
+ from mecosphere import components
9
+ from mecosphere.simulation import *
10
+ from mecosphere.tilemap import *
11
+
12
+
13
+ def arguments_create() -> Namespace:
14
+ argument_parser = argparse.ArgumentParser(prog="Maeve's Ecosphere")
15
+ argument_parser.add_argument(
16
+ "-c",
17
+ "--cycles",
18
+ type=int,
19
+ help="Limit simulation to a number of cycles.",
20
+ )
21
+ argument_parser.add_argument(
22
+ "-x",
23
+ "--width",
24
+ default=64,
25
+ type=int,
26
+ help="Width in tiles, this value is multiplied by 16 for the display window's width.",
27
+ )
28
+ argument_parser.add_argument(
29
+ "-y",
30
+ "--height",
31
+ default=36,
32
+ type=int,
33
+ help="Height in tiles, this value is multiplied by 16 for the display window's height.",
34
+ )
35
+ argument_parser.add_argument(
36
+ "-z",
37
+ "--headless",
38
+ action="store_true",
39
+ help="Height in tiles, this value is multiplied by 16 for the display window's height.",
40
+ )
41
+ arguments = argument_parser.parse_args()
42
+
43
+ return arguments
44
+
45
+
46
+ class ApplicationSettings:
47
+ def __init__(self):
48
+ self.arguments = arguments_create()
49
+
50
+ if self.arguments.cycles:
51
+ self.cycles: int = self.arguments.cycles
52
+ else:
53
+ self.cycles = -1
54
+
55
+ self.headless: bool = self.arguments.headless
56
+ self.height: int = self.arguments.height
57
+ self.width: int = self.arguments.width
58
+
59
+
60
+ class EcosphereApplication:
61
+ def __init__(self):
62
+ self.should_run = True
63
+ self.should_simulate = True
64
+
65
+ self.application_settings = ApplicationSettings()
66
+
67
+ if not self.application_settings.headless:
68
+ pygame.init()
69
+
70
+ self.pygame_display = pygame.display.set_mode(
71
+ (
72
+ self.application_settings.width * 16,
73
+ self.application_settings.height * 16,
74
+ )
75
+ )
76
+
77
+ if not self.application_settings.headless:
78
+ self.tile_dirt = pygame.image.load("res/img/dirt.bmp").convert()
79
+
80
+ self.ecosphere = Ecosphere(self.application_settings)
81
+
82
+ self.initialize()
83
+
84
+ def tiles_render(self):
85
+ col_index = 0
86
+ row_index = 0
87
+
88
+ for collumn in self.ecosphere.map:
89
+ for row in collumn:
90
+ tile = self.ecosphere.map[col_index][row_index]
91
+
92
+ self.pygame_display.blit(
93
+ self.tile_dirt, (row_index * 16, col_index * 16)
94
+ )
95
+
96
+ if tile.occupant:
97
+ self.pygame_display.blit(
98
+ tile.occupant.image, (row_index * 16, col_index * 16)
99
+ )
100
+
101
+ row_index += 1
102
+
103
+ row_index = 0
104
+ col_index += 1
105
+
106
+ def run(self):
107
+ cycles: int = 0
108
+
109
+ print("Running application!")
110
+
111
+ try:
112
+ while self.should_run:
113
+ self.pygame_display.fill((0, 0, 0))
114
+
115
+ for event in pygame.event.get():
116
+ if event.type == pygame.QUIT:
117
+ self.should_run = False
118
+
119
+ if self.should_simulate:
120
+ if self.ecosphere.update() == 1:
121
+ self.should_simulate = False
122
+ elif not self.application_settings.cycles == -1:
123
+ if cycles >= self.application_settings.cycles:
124
+ self.should_simulate = False
125
+ self.should_run = False
126
+
127
+ cycles += 1
128
+
129
+ print(f"Simulation cycle {cycles}\r", end="")
130
+
131
+ self.tiles_render()
132
+
133
+ pygame.display.flip()
134
+
135
+ # time.sleep(0.1666)
136
+ except KeyboardInterrupt:
137
+ print(f"Simulation cycle {cycles}\r", end="")
138
+ self.should_simulate = False
139
+ self.should_run = False
140
+
141
+ print(f"Exited application after {cycles-1} cycles.")
142
+
143
+ pygame.quit()
144
+
145
+ def run_headless(self):
146
+ cycles: int = 0
147
+
148
+ print("Running application in headless mode!")
149
+
150
+ try:
151
+ while self.should_run:
152
+ if self.should_simulate:
153
+ if self.ecosphere.update() == 1:
154
+ self.should_simulate = False
155
+ elif not self.application_settings.cycles == -1:
156
+ if cycles >= self.application_settings.cycles:
157
+ self.should_simulate = False
158
+ self.should_run = False
159
+
160
+ cycles += 1
161
+
162
+ print(f"Simulation cycle {cycles}\r", end="")
163
+
164
+ except KeyboardInterrupt:
165
+ print(f"Simulation cycle {cycles}\r", end="")
166
+ self.should_run = False
167
+
168
+ print(f"Exited application after {cycles-1} cycles.")
169
+
170
+ def start(self):
171
+ if not self.application_settings.headless:
172
+ self.run()
173
+ else:
174
+ self.run_headless()
175
+
176
+ def initialize(self):
177
+ # Propogation
178
+ print("Propogating ecosphere...")
179
+
180
+ propogated_map = self.ecosphere.map.copy()
181
+
182
+ for tile, col_index, row_index in ecosphere_map_yield_tile(
183
+ self.ecosphere.map,
184
+ width=self.application_settings.width,
185
+ height=self.application_settings.height,
186
+ ):
187
+ tile = propogated_map[col_index][row_index]
188
+
189
+ if tile == 1:
190
+ new_tree = SimulationObject(
191
+ components=[],
192
+ domain="flora",
193
+ age_maximum=random.randint(1825, 7250),
194
+ name="tree",
195
+ species="tree",
196
+ images={"image_egg": "seed"},
197
+ position_range_width=self.application_settings.width,
198
+ position_range_height=self.application_settings.height,
199
+ headless=self.application_settings.headless,
200
+ component_data={
201
+ "Aging": {
202
+ "is_egg": False,
203
+ "is_baby": False,
204
+ "age_baby": 180,
205
+ "age_adult": 356,
206
+ },
207
+ "ReproducingAsexually": {
208
+ "rate_percentage": 50,
209
+ "while_baby": False,
210
+ },
211
+ },
212
+ position=[col_index, row_index],
213
+ )
214
+
215
+ new_tree.components.append(components.component_age)
216
+ new_tree.components.append(components.component_reproduce_asexual)
217
+
218
+ new_tree.decomposition_maximum = 1000
219
+
220
+ propogated_map[col_index][row_index] = Tile(
221
+ occupant=new_tree,
222
+ nutritious=False,
223
+ contaminated=False,
224
+ pos_x=col_index,
225
+ pos_y=row_index,
226
+ )
227
+
228
+ self.ecosphere.simulation_objects.append(new_tree)
229
+ elif tile >= 2 and tile < 5:
230
+ new_tree = SimulationObject(
231
+ components=[],
232
+ domain="flora",
233
+ age_maximum=random.randint(120, 270),
234
+ name="grass",
235
+ species="grass",
236
+ position_range_width=self.application_settings.width,
237
+ position_range_height=self.application_settings.height,
238
+ headless=self.application_settings.headless,
239
+ images={"image_egg": "seed"},
240
+ component_data={
241
+ "Aging": {
242
+ "is_egg": False,
243
+ "is_baby": False,
244
+ "age_baby": 5,
245
+ "age_adult": 15,
246
+ },
247
+ "ReproducingAsexually": {
248
+ "rate_percentage": 95,
249
+ "while_baby": True,
250
+ },
251
+ },
252
+ position=[col_index, row_index],
253
+ )
254
+
255
+ new_tree.components.append(components.component_age)
256
+ new_tree.components.append(components.component_reproduce_asexual)
257
+
258
+ propogated_map[col_index][row_index] = Tile(
259
+ occupant=new_tree,
260
+ nutritious=False,
261
+ contaminated=False,
262
+ pos_x=col_index,
263
+ pos_y=row_index,
264
+ )
265
+
266
+ self.ecosphere.simulation_objects.append(new_tree)
267
+ elif tile == 5:
268
+ new_tree = SimulationObject(
269
+ components=[],
270
+ domain="fungus",
271
+ age_maximum=random.randint(1500, 3000),
272
+ name=f"mushroom",
273
+ species="mushroom",
274
+ position_range_width=self.application_settings.width,
275
+ position_range_height=self.application_settings.height,
276
+ headless=self.application_settings.headless,
277
+ images={"image_egg": "seed"},
278
+ component_data={
279
+ "Aging": {
280
+ "is_egg": False,
281
+ "is_baby": False,
282
+ "age_baby": 500,
283
+ "age_adult": 1000,
284
+ },
285
+ "ReproducingAsexually": {
286
+ "rate_percentage": 90,
287
+ "while_baby": True,
288
+ },
289
+ },
290
+ position=[col_index, row_index],
291
+ )
292
+
293
+ new_tree.components.append(components.component_age)
294
+ new_tree.components.append(components.component_reproduce_asexual)
295
+
296
+ propogated_map[col_index][row_index] = Tile(
297
+ occupant=new_tree,
298
+ nutritious=False,
299
+ contaminated=False,
300
+ pos_x=col_index,
301
+ pos_y=row_index,
302
+ )
303
+
304
+ self.ecosphere.simulation_objects.append(new_tree)
305
+ else:
306
+ propogated_map[col_index][row_index] = Tile(
307
+ occupant=None,
308
+ nutritious=False,
309
+ contaminated=False,
310
+ pos_x=col_index,
311
+ pos_y=row_index,
312
+ )
313
+
314
+ row_index += 1
315
+
316
+ row_index = 0
317
+ col_index += 1
318
+
319
+ self.ecosphere.map = propogated_map
320
+
321
+ for i in range(50):
322
+ isopod = SimulationObject(
323
+ components=[],
324
+ domain="fauna",
325
+ age_maximum=random.randint(913, 1825),
326
+ name=f"isopod {i}",
327
+ species="isopod",
328
+ position_range_width=self.application_settings.width,
329
+ position_range_height=self.application_settings.height,
330
+ headless=self.application_settings.headless,
331
+ component_data={
332
+ "Eating": {
333
+ "diet": ["grass", "mushroom"],
334
+ "hunger": 0,
335
+ "hunger_max": 120,
336
+ },
337
+ "Aging": {
338
+ "is_egg": True,
339
+ "is_baby": False,
340
+ "age_baby": 30,
341
+ "age_adult": 60,
342
+ },
343
+ },
344
+ images={"image_baby": "isopod-baby"},
345
+ )
346
+
347
+ if not self.application_settings.headless:
348
+ isopod.image = isopod.image_baby
349
+
350
+ isopod.components.append(components.component_forage)
351
+ isopod.components.append(components.component_eat)
352
+ isopod.components.append(components.component_age)
353
+ isopod.components.append(components.component_reproduce_sexual_direct)
354
+
355
+ self.ecosphere.simulation_objects.append(isopod)
356
+ self.ecosphere.map[isopod.position[0]][isopod.position[1]].occupant = isopod
@@ -0,0 +1,31 @@
1
+ #!/usr/local/bin/env python3
2
+
3
+ """
4
+ A simple ecosphere simulator I made while bored and sick.
5
+ """
6
+
7
+ import sys
8
+ import random
9
+ import csv
10
+ import time
11
+
12
+ import pygame
13
+
14
+ from mecosphere.application import *
15
+ from mecosphere.simulation import *
16
+ from mecosphere.tilemap import *
17
+
18
+
19
+ def main():
20
+ """
21
+ The entrypoint for the script.
22
+ """
23
+
24
+ application = EcosphereApplication()
25
+
26
+ application.start()
27
+ application.should_run = False
28
+
29
+
30
+ if __name__ == "__main__":
31
+ main()
@@ -0,0 +1,323 @@
1
+ import sys
2
+ import random
3
+ import time
4
+ import copy
5
+
6
+ from mecosphere.simulation import *
7
+
8
+
9
+ def create_descendant(
10
+ simobject_instance, new_position, _position_range_width, _position_range_height
11
+ ) -> SimulationObject:
12
+ simobject_descendant = SimulationObject(
13
+ name=f"{simobject_instance.name} Jr.",
14
+ species=simobject_instance.species,
15
+ domain=simobject_instance.domain,
16
+ components=copy.deepcopy(simobject_instance.components),
17
+ component_data=copy.deepcopy(simobject_instance.component_data),
18
+ age_maximum=simobject_instance.age_maximum,
19
+ images=simobject_instance.images,
20
+ position=new_position,
21
+ position_range_width=_position_range_width,
22
+ position_range_height=_position_range_height,
23
+ headless=simobject_instance.headless,
24
+ )
25
+
26
+ simobject_descendant.age_current = 0
27
+
28
+ return simobject_descendant
29
+
30
+
31
+ def create_component(component_name, callback: callable, is_active=True) -> dict:
32
+ """
33
+ A simple constructor for components as dictionaries. This is not an ECS,
34
+ just borrowing the language.
35
+ """
36
+
37
+ component_dictionary = {
38
+ "name": component_name,
39
+ "active": is_active,
40
+ "callback": callback,
41
+ }
42
+
43
+ return component_dictionary
44
+
45
+
46
+ def system_age(
47
+ simobject_instance, component_instance, ecosphere_map, application_settings
48
+ ):
49
+ if simobject_instance.is_dead:
50
+ return
51
+
52
+ age_current = simobject_instance.age_current
53
+ age_baby = simobject_instance.component_data["Aging"]["age_baby"]
54
+ age_adult = simobject_instance.component_data["Aging"]["age_adult"]
55
+
56
+ if age_current < age_baby:
57
+ if not application_settings.headless:
58
+ simobject_instance.image = simobject_instance.image_egg
59
+ simobject_instance.component_data["Aging"]["is_egg"] = True
60
+ simobject_instance.component_data["Aging"]["is_baby"] = False
61
+ elif age_current >= age_adult:
62
+ if not application_settings.headless:
63
+ simobject_instance.image = simobject_instance.image_default
64
+ simobject_instance.component_data["Aging"]["is_egg"] = False
65
+ simobject_instance.component_data["Aging"]["is_baby"] = False
66
+ elif age_current > age_baby and age_current < age_adult:
67
+ if not application_settings.headless:
68
+ simobject_instance.image = simobject_instance.image_baby
69
+ simobject_instance.component_data["Aging"]["is_egg"] = False
70
+ simobject_instance.component_data["Aging"]["is_baby"] = True
71
+ else:
72
+ if not application_settings.headless:
73
+ simobject_instance.image = simobject_instance.image_default
74
+
75
+
76
+ def system_forage(
77
+ simobject_instance, component_instance, ecosphere_map, application_settings
78
+ ):
79
+ if "Aging" in simobject_instance.component_data:
80
+ if simobject_instance.component_data["Aging"]["is_egg"]:
81
+ return
82
+
83
+ position_old_tile = ecosphere_map[simobject_instance.position[0]][
84
+ simobject_instance.position[1]
85
+ ]
86
+
87
+ position_old_tile.occupant = simobject_instance
88
+
89
+ position_new = simobject_instance.position.copy()
90
+
91
+ position_new = [
92
+ position_new[0] + random.randint(-1, 1),
93
+ position_new[1] + random.randint(-1, 1),
94
+ ]
95
+
96
+ if position_new[0] > (application_settings.height - 1) or position_new[0] < 0:
97
+ return
98
+ elif position_new[1] > (application_settings.width - 1) or position_new[1] < 0:
99
+ return
100
+ else:
101
+ position_new_tile = ecosphere_map[position_new[0]][position_new[1]]
102
+
103
+ if not position_new_tile.occupant:
104
+ position_old_tile.occupant = None
105
+
106
+ simobject_instance.position = position_new
107
+
108
+ position_new_tile.occupant = simobject_instance
109
+
110
+
111
+ def system_eat(
112
+ simobject_instance, component_instance, ecosphere_map, application_settings
113
+ ):
114
+ def eat():
115
+ if (
116
+ search_tile.occupant.species
117
+ in simobject_instance.component_data[component_instance["name"]]["diet"]
118
+ ):
119
+ search_tile.occupant.die()
120
+
121
+ hunger = simobject_instance.component_data[component_instance["name"]]["hunger"]
122
+ hunger_max = simobject_instance.component_data[component_instance["name"]][
123
+ "hunger_max"
124
+ ]
125
+
126
+ hunger += 1
127
+
128
+ if hunger >= hunger_max:
129
+ simobject_instance.die()
130
+ return
131
+ else:
132
+ simobject_instance.component_data[component_instance["name"]]["hunger"] = hunger
133
+
134
+ for search_tile, pos_x, pos_y in simulation_object_yield_tile_neighbour(
135
+ simobject_instance,
136
+ ecosphere_map,
137
+ width=application_settings.width,
138
+ height=application_settings.height,
139
+ ):
140
+ if search_tile.occupant:
141
+ if search_tile.occupant == simobject_instance:
142
+ return
143
+ elif search_tile.occupant.species == simobject_instance.species:
144
+ return
145
+ else:
146
+ eat()
147
+ simobject_instance.component_data[component_instance["name"]][
148
+ "hunger"
149
+ ] = 0
150
+
151
+
152
+ def is_hungry(_simobject_instance):
153
+ hungry = False
154
+
155
+ if "Eating" in _simobject_instance.component_data:
156
+ hunger = _simobject_instance.component_data["Eating"]["hunger"]
157
+ hunger_max = _simobject_instance.component_data["Eating"]["hunger_max"]
158
+
159
+ if hunger > (int(round(hunger_max / 2))):
160
+ hungry = True
161
+ return hungry
162
+
163
+ return hungry
164
+
165
+
166
+ def system_reproduce_sexual_direct(
167
+ simobject_instance, component_instance, ecosphere_map, application_settings
168
+ ):
169
+ reproduction_partner = None
170
+
171
+ if "Aging" in simobject_instance.component_data:
172
+ if simobject_instance.component_data["Aging"]["is_egg"]:
173
+ return
174
+ elif simobject_instance.component_data["Aging"]["is_baby"]:
175
+ return
176
+
177
+ if is_hungry(simobject_instance):
178
+ return
179
+
180
+ for search_tile, pos_x, pos_y in simulation_object_yield_tile_neighbour(
181
+ simobject_instance,
182
+ ecosphere_map,
183
+ width=application_settings.width,
184
+ height=application_settings.height,
185
+ ):
186
+ if search_tile.occupant:
187
+ if not search_tile.occupant == simobject_instance:
188
+ if not search_tile.occupant.is_dead:
189
+ reproduction_partner = search_tile.occupant
190
+ break
191
+
192
+ if reproduction_partner:
193
+ if is_hungry(reproduction_partner):
194
+ return
195
+ else:
196
+ for search_tile, pos_x, pos_y in simulation_object_yield_tile_neighbour(
197
+ simobject_instance,
198
+ ecosphere_map,
199
+ width=application_settings.width,
200
+ height=application_settings.height,
201
+ ):
202
+ if (
203
+ not search_tile.occupant
204
+ and not search_tile.metadata["contaminated"]
205
+ ):
206
+ baby = create_descendant(
207
+ simobject_instance,
208
+ new_position=[pos_x, pos_y],
209
+ _position_range_width=application_settings.width,
210
+ _position_range_height=application_settings.height,
211
+ ) # Awww they're so alike
212
+
213
+ baby.is_egg = True
214
+ baby.is_baby = False
215
+
216
+ if "Eating" in baby.component_data:
217
+ baby_hunger_max = baby.component_data["Eating"]["hunger_max"]
218
+
219
+ # Set the baby's hunger to 1/4 max hunger
220
+ baby.component_data["Eating"]["hunger"] = int(
221
+ round(baby_hunger_max / 4)
222
+ )
223
+
224
+ baby.age_current = 0
225
+
226
+ ecosphere_map[pos_x][pos_y].occupant = baby
227
+
228
+ if "Eating" in search_tile.occupant.component_data:
229
+ search_tile.occupant.component_data["Eating"]["hunger"] += int(
230
+ round(
231
+ search_tile.occupant.component_data["Eating"][
232
+ "hunger_max"
233
+ ]
234
+ / 3
235
+ )
236
+ )
237
+ if "Eating" in simobject_instance.component_data:
238
+ simobject_instance.component_data["Eating"]["hunger"] += int(
239
+ round(
240
+ simobject_instance.component_data["Eating"][
241
+ "hunger_max"
242
+ ]
243
+ / 2
244
+ )
245
+ )
246
+
247
+
248
+ def system_reproduce_asexual(
249
+ simobject_instance, component_instance, ecosphere_map, application_settings
250
+ ):
251
+ while_baby = simobject_instance.component_data["ReproducingAsexually"]["while_baby"]
252
+
253
+ rate_percentage = simobject_instance.component_data["ReproducingAsexually"][
254
+ "rate_percentage"
255
+ ]
256
+
257
+ rate_roll = random.randint(1, 100)
258
+
259
+ if rate_roll > (int(100 - round(rate_percentage))):
260
+ return
261
+
262
+ if not while_baby:
263
+ if "Aging" in simobject_instance.component_data:
264
+ if simobject_instance.component_data["Aging"]["is_egg"]:
265
+ return
266
+ if simobject_instance.component_data["Aging"]["is_baby"]:
267
+ return
268
+
269
+ for search_tile, pos_x, pos_y in simulation_object_yield_tile_neighbour(
270
+ simobject_instance,
271
+ ecosphere_map,
272
+ width=application_settings.width,
273
+ height=application_settings.height,
274
+ ):
275
+ if search_tile.occupant:
276
+ if search_tile.occupant == simobject_instance:
277
+ continue
278
+ elif not search_tile.occupant.is_dead:
279
+ continue
280
+ elif search_tile.occupant.species == simobject_instance.species:
281
+ continue
282
+ else:
283
+ clone = create_descendant(
284
+ simobject_instance,
285
+ new_position=[pos_x, pos_y],
286
+ _position_range_width=application_settings.width,
287
+ _position_range_height=application_settings.height,
288
+ ) # Propogation
289
+
290
+ clone.is_egg = True
291
+ clone.is_baby = False
292
+
293
+ clone.age_current = 0
294
+
295
+ ecosphere_map[pos_x][pos_y].occupant = clone
296
+
297
+ if search_tile.metadata["contaminated"]:
298
+ ecosphere_map[pos_x][pos_y].metadata["contaminated"] = False
299
+
300
+
301
+ component_forage: dict = create_component(
302
+ component_name="Foraging", callback=system_forage
303
+ )
304
+
305
+ component_eat: dict = create_component(
306
+ component_name="Eating",
307
+ callback=system_eat,
308
+ )
309
+
310
+ component_reproduce_sexual_direct: dict = create_component(
311
+ component_name="ReproducingSexuallyDirectly",
312
+ callback=system_reproduce_sexual_direct,
313
+ )
314
+
315
+ component_reproduce_asexual: dict = create_component(
316
+ component_name="ReproducingAsexually",
317
+ callback=system_reproduce_asexual,
318
+ )
319
+
320
+ component_age: dict = create_component(
321
+ component_name="Aging",
322
+ callback=system_age,
323
+ )
@@ -0,0 +1,160 @@
1
+ import sys
2
+ import random
3
+ import time
4
+
5
+ import pygame
6
+
7
+ from mecosphere.tilemap import *
8
+
9
+
10
+ class Tile(object):
11
+ __slots__ = ("occupant", "metadata", "pos_x", "pos_y")
12
+
13
+ def __init__(self, occupant, nutritious, contaminated, pos_x, pos_y):
14
+ self.occupant: SimulationObject = occupant
15
+ self.pos_x = pos_x
16
+ self.pos_y = pos_y
17
+ self.metadata = self.metadata_create(nutritious, contaminated)
18
+
19
+ def metadata_create(self, nutritious=False, contaminated=False) -> dict:
20
+ metadata_dictionary = {"nutritious": nutritious, "contaminated": contaminated}
21
+
22
+ return metadata_dictionary
23
+
24
+
25
+ class SimulationObject:
26
+ def __init__(
27
+ self,
28
+ name,
29
+ species,
30
+ domain,
31
+ position_range_width,
32
+ position_range_height,
33
+ components: list,
34
+ age_maximum=7,
35
+ component_data={},
36
+ images={},
37
+ position=[],
38
+ headless=False,
39
+ ):
40
+ self.age_current = 0
41
+ self.age_maximum = age_maximum
42
+ self.decomposition_current = 0
43
+ self.decomposition_maximum = int(round(age_maximum / 8))
44
+ self.components: list = components
45
+ self.images = images
46
+ self.name = name
47
+ self.species = species
48
+ self.position_range_width = position_range_width
49
+ self.position_range_height = position_range_height
50
+ self.headless = headless
51
+
52
+ if not position:
53
+ self.position = [
54
+ random.randint(1, self.position_range_height - 1),
55
+ random.randint(1, self.position_range_width - 1),
56
+ ]
57
+ else:
58
+ self.position = position
59
+
60
+ self.domain = domain
61
+ self.is_dead = False
62
+
63
+ if not self.headless:
64
+ self.image_default = pygame.image.load(f"res/img/{species}.bmp")
65
+ self.image_egg = pygame.image.load(f"res/img/egg.bmp")
66
+ self.image_dead = pygame.image.load(f"res/img/{species}-dead.bmp")
67
+ self.image_baby = self.image_default
68
+
69
+ if "image_baby" in images:
70
+ self.image_baby = pygame.image.load(
71
+ f"res/img/{images['image_baby']}.bmp"
72
+ )
73
+ elif "image_egg" in images:
74
+ self.image_egg = pygame.image.load(f"res/img/{images['image_egg']}.bmp")
75
+
76
+ self.image = self.image_default
77
+
78
+ # `self.component_data`` is a dict of component names matched to any other child dict
79
+ # Example:
80
+ # { "Eating": { "diet": [ "grass", "mushroom" ] }, }
81
+
82
+ self.component_data = component_data
83
+
84
+ def die(self):
85
+ if not self.headless:
86
+ self.image = self.image_dead
87
+
88
+ self.is_dead = True
89
+
90
+
91
+ class Ecosphere:
92
+ def __init__(self, _application_settings):
93
+ self.simulation_objects = (
94
+ []
95
+ ) # Set on every update call, do not change manually.
96
+ self.fauna: list = []
97
+ self.flora: list = []
98
+ self.bacteria: list = []
99
+ self.viruses: list = []
100
+ self.time = 0.0
101
+ self.application_settings = _application_settings
102
+ self.map = ecosphere_map_generate(self.application_settings)
103
+
104
+ def update(self) -> int:
105
+ if self.simulation_objects:
106
+ # Old list may contain out of bounds entitities
107
+ # We set self.simulation_objects to this new list in order to remove them
108
+ new_entities_list = []
109
+ living_entities_count = 0
110
+
111
+ for tile, col_index, row_index in ecosphere_map_yield_tile(
112
+ self.map,
113
+ width=self.application_settings.width,
114
+ height=self.application_settings.height,
115
+ ):
116
+ simulation_object = tile.occupant
117
+
118
+ if simulation_object:
119
+ new_entities_list.append(simulation_object)
120
+
121
+ if simulation_object.age_current > simulation_object.age_maximum:
122
+ if not simulation_object.is_dead:
123
+ simulation_object.die() # Messed up, man. :C
124
+ elif (
125
+ simulation_object.decomposition_current
126
+ >= simulation_object.decomposition_maximum
127
+ ):
128
+ if simulation_object.species == "fauna":
129
+ self.map[col_index][row_index].contaminated = True
130
+
131
+ self.map[col_index][row_index].occupant = None
132
+
133
+ self.simulation_objects.remove(simulation_object)
134
+
135
+ del simulation_object
136
+ else:
137
+ simulation_object.decomposition_current += 1
138
+ else:
139
+ living_entities_count += 1
140
+ simulation_object.age_current += 1
141
+
142
+ if (
143
+ simulation_object.components
144
+ and not simulation_object.is_dead
145
+ ):
146
+ for component in simulation_object.components:
147
+ component["callback"](
148
+ simulation_object,
149
+ component,
150
+ self.map,
151
+ self.application_settings,
152
+ )
153
+
154
+ self.simulation_objects = new_entities_list
155
+ else:
156
+ print("No more simulation objects, ending simulation.")
157
+
158
+ return 1
159
+
160
+ return 0
@@ -0,0 +1,69 @@
1
+ import sys
2
+ import random
3
+ import time
4
+
5
+ import pygame
6
+
7
+
8
+ def simulation_object_yield_tile_neighbour(
9
+ _simobject_instance, _ecosphere_map, width, height
10
+ ):
11
+ directions = [[-1, 0], [1, 0], [0, 1], [0, -1]] # Left # Right # Up # Down
12
+
13
+ for direction in directions:
14
+ check_position_x = _simobject_instance.position[0] + direction[0]
15
+ check_position_y = _simobject_instance.position[1] + direction[1]
16
+
17
+ if check_position_y > (width - 1):
18
+ check_position_y = width - 1
19
+ elif check_position_y < 0:
20
+ check_position_y = 0
21
+
22
+ if check_position_x > (height - 1):
23
+ check_position_x = height - 1
24
+ elif check_position_x < 0:
25
+ check_position_x = 0
26
+
27
+ tile = _ecosphere_map[check_position_x][check_position_y]
28
+
29
+ yield tile, check_position_x, check_position_y
30
+
31
+
32
+ def ecosphere_map_yield_tile(ecosphere_map, width, height):
33
+ position_x = 0
34
+ position_y = 0
35
+
36
+ for collumn in range(height):
37
+ for row in range(width):
38
+ yield ecosphere_map[collumn][row], position_x, position_y
39
+
40
+ position_y += 1
41
+
42
+ position_y = 0
43
+ position_x += 1
44
+
45
+
46
+ def ecosphere_map_generate(_application_settings) -> list:
47
+ print("Generating map...")
48
+ new_map = []
49
+ position_x = 0
50
+ position_y = 0
51
+
52
+ for collumn in range(_application_settings.height):
53
+ new_collumn = []
54
+
55
+ for row in range(_application_settings.width):
56
+ place_vegetation = bool(random.getrandbits(1))
57
+
58
+ if place_vegetation:
59
+ new_collumn.append(random.randint(1, 5))
60
+ else:
61
+ new_collumn.append(0)
62
+
63
+ position_y += 1
64
+
65
+ new_map.append(new_collumn)
66
+
67
+ position_x += 1
68
+
69
+ return new_map