|
| 1 | +""" |
| 2 | +Wa-Tor algorithm (1984) |
| 3 | +
|
| 4 | +@ https://en.wikipedia.org/wiki/Wa-Tor |
| 5 | +@ https://beltoforion.de/en/wator/ |
| 6 | +@ https://beltoforion.de/en/wator/images/wator_medium.webm |
| 7 | +
|
| 8 | +This solution aims to completely remove any systematic approach |
| 9 | +to the Wa-Tor planet, and utilise fully random methods. |
| 10 | +
|
| 11 | +The constants are a working set that allows the Wa-Tor planet |
| 12 | +to result in one of the three possible results. |
| 13 | +""" |
| 14 | + |
| 15 | +from collections.abc import Callable |
| 16 | +from random import randint, shuffle |
| 17 | +from time import sleep |
| 18 | +from typing import Literal |
| 19 | + |
| 20 | +WIDTH = 50 # Width of the Wa-Tor planet |
| 21 | +HEIGHT = 50 # Height of the Wa-Tor planet |
| 22 | + |
| 23 | +PREY_INITIAL_COUNT = 30 # The initial number of prey entities |
| 24 | +PREY_REPRODUCTION_TIME = 5 # The chronons before reproducing |
| 25 | + |
| 26 | +PREDATOR_INITIAL_COUNT = 50 # The initial number of predator entities |
| 27 | +# The initial energy value of predator entities |
| 28 | +PREDATOR_INITIAL_ENERGY_VALUE = 15 |
| 29 | +# The energy value provided when consuming prey |
| 30 | +PREDATOR_FOOD_VALUE = 5 |
| 31 | +PREDATOR_REPRODUCTION_TIME = 20 # The chronons before reproducing |
| 32 | + |
| 33 | +MAX_ENTITIES = 500 # The max number of organisms on the board |
| 34 | +# The number of entities to delete from the unbalanced side |
| 35 | +DELETE_UNBALANCED_ENTITIES = 50 |
| 36 | + |
| 37 | + |
| 38 | +class Entity: |
| 39 | + """ |
| 40 | + Represents an entity (either prey or predator). |
| 41 | +
|
| 42 | + >>> e = Entity(True, coords=(0, 0)) |
| 43 | + >>> e.prey |
| 44 | + True |
| 45 | + >>> e.coords |
| 46 | + (0, 0) |
| 47 | + >>> e.alive |
| 48 | + True |
| 49 | + """ |
| 50 | + |
| 51 | + def __init__(self, prey: bool, coords: tuple[int, int]) -> None: |
| 52 | + self.prey = prey |
| 53 | + # The (row, col) pos of the entity |
| 54 | + self.coords = coords |
| 55 | + |
| 56 | + self.remaining_reproduction_time = ( |
| 57 | + PREY_REPRODUCTION_TIME if prey else PREDATOR_REPRODUCTION_TIME |
| 58 | + ) |
| 59 | + self.energy_value = None if prey is True else PREDATOR_INITIAL_ENERGY_VALUE |
| 60 | + self.alive = True |
| 61 | + |
| 62 | + def reset_reproduction_time(self) -> None: |
| 63 | + """ |
| 64 | + >>> e = Entity(True, coords=(0, 0)) |
| 65 | + >>> e.reset_reproduction_time() |
| 66 | + >>> e.remaining_reproduction_time == PREY_REPRODUCTION_TIME |
| 67 | + True |
| 68 | + >>> e = Entity(False, coords=(0, 0)) |
| 69 | + >>> e.reset_reproduction_time() |
| 70 | + >>> e.remaining_reproduction_time == PREDATOR_REPRODUCTION_TIME |
| 71 | + True |
| 72 | + """ |
| 73 | + self.remaining_reproduction_time = ( |
| 74 | + PREY_REPRODUCTION_TIME if self.prey is True else PREDATOR_REPRODUCTION_TIME |
| 75 | + ) |
| 76 | + |
| 77 | + def __repr__(self) -> str: |
| 78 | + """ |
| 79 | + >>> Entity(prey=True, coords=(1, 1)) |
| 80 | + Entity(prey=True, coords=(1, 1), remaining_reproduction_time=5) |
| 81 | + >>> Entity(prey=False, coords=(2, 1)) # doctest: +NORMALIZE_WHITESPACE |
| 82 | + Entity(prey=False, coords=(2, 1), |
| 83 | + remaining_reproduction_time=20, energy_value=15) |
| 84 | + """ |
| 85 | + repr_ = ( |
| 86 | + f"Entity(prey={self.prey}, coords={self.coords}, " |
| 87 | + f"remaining_reproduction_time={self.remaining_reproduction_time}" |
| 88 | + ) |
| 89 | + if self.energy_value is not None: |
| 90 | + repr_ += f", energy_value={self.energy_value}" |
| 91 | + return f"{repr_})" |
| 92 | + |
| 93 | + |
| 94 | +class WaTor: |
| 95 | + """ |
| 96 | + Represents the main Wa-Tor algorithm. |
| 97 | +
|
| 98 | + :attr time_passed: A function that is called every time |
| 99 | + time passes (a chronon) in order to visually display |
| 100 | + the new Wa-Tor planet. The time_passed function can block |
| 101 | + using time.sleep to slow the algorithm progression. |
| 102 | +
|
| 103 | + >>> wt = WaTor(10, 15) |
| 104 | + >>> wt.width |
| 105 | + 10 |
| 106 | + >>> wt.height |
| 107 | + 15 |
| 108 | + >>> len(wt.planet) |
| 109 | + 15 |
| 110 | + >>> len(wt.planet[0]) |
| 111 | + 10 |
| 112 | + >>> len(wt.get_entities()) == PREDATOR_INITIAL_COUNT + PREY_INITIAL_COUNT |
| 113 | + True |
| 114 | + """ |
| 115 | + |
| 116 | + time_passed: Callable[["WaTor", int], None] | None |
| 117 | + |
| 118 | + def __init__(self, width: int, height: int) -> None: |
| 119 | + self.width = width |
| 120 | + self.height = height |
| 121 | + self.time_passed = None |
| 122 | + |
| 123 | + self.planet: list[list[Entity | None]] = [[None] * width for _ in range(height)] |
| 124 | + |
| 125 | + # Populate planet with predators and prey randomly |
| 126 | + for _ in range(PREY_INITIAL_COUNT): |
| 127 | + self.add_entity(prey=True) |
| 128 | + for _ in range(PREDATOR_INITIAL_COUNT): |
| 129 | + self.add_entity(prey=False) |
| 130 | + self.set_planet(self.planet) |
| 131 | + |
| 132 | + def set_planet(self, planet: list[list[Entity | None]]) -> None: |
| 133 | + """ |
| 134 | + Ease of access for testing |
| 135 | +
|
| 136 | + >>> wt = WaTor(WIDTH, HEIGHT) |
| 137 | + >>> planet = [ |
| 138 | + ... [None, None, None], |
| 139 | + ... [None, Entity(True, coords=(1, 1)), None] |
| 140 | + ... ] |
| 141 | + >>> wt.set_planet(planet) |
| 142 | + >>> wt.planet == planet |
| 143 | + True |
| 144 | + >>> wt.width |
| 145 | + 3 |
| 146 | + >>> wt.height |
| 147 | + 2 |
| 148 | + """ |
| 149 | + self.planet = planet |
| 150 | + self.width = len(planet[0]) |
| 151 | + self.height = len(planet) |
| 152 | + |
| 153 | + def add_entity(self, prey: bool) -> None: |
| 154 | + """ |
| 155 | + Adds an entity, making sure the entity does |
| 156 | + not override another entity |
| 157 | +
|
| 158 | + >>> wt = WaTor(WIDTH, HEIGHT) |
| 159 | + >>> wt.set_planet([[None, None], [None, None]]) |
| 160 | + >>> wt.add_entity(True) |
| 161 | + >>> len(wt.get_entities()) |
| 162 | + 1 |
| 163 | + >>> wt.add_entity(False) |
| 164 | + >>> len(wt.get_entities()) |
| 165 | + 2 |
| 166 | + """ |
| 167 | + while True: |
| 168 | + row, col = randint(0, self.height - 1), randint(0, self.width - 1) |
| 169 | + if self.planet[row][col] is None: |
| 170 | + self.planet[row][col] = Entity(prey=prey, coords=(row, col)) |
| 171 | + return |
| 172 | + |
| 173 | + def get_entities(self) -> list[Entity]: |
| 174 | + """ |
| 175 | + Returns a list of all the entities within the planet. |
| 176 | +
|
| 177 | + >>> wt = WaTor(WIDTH, HEIGHT) |
| 178 | + >>> len(wt.get_entities()) == PREDATOR_INITIAL_COUNT + PREY_INITIAL_COUNT |
| 179 | + True |
| 180 | + """ |
| 181 | + return [entity for column in self.planet for entity in column if entity] |
| 182 | + |
| 183 | + def balance_predators_and_prey(self) -> None: |
| 184 | + """ |
| 185 | + Balances predators and preys so that prey |
| 186 | + can not dominate the predators, blocking up |
| 187 | + space for them to reproduce. |
| 188 | +
|
| 189 | + >>> wt = WaTor(WIDTH, HEIGHT) |
| 190 | + >>> for i in range(2000): |
| 191 | + ... row, col = i // HEIGHT, i % WIDTH |
| 192 | + ... wt.planet[row][col] = Entity(True, coords=(row, col)) |
| 193 | + >>> entities = len(wt.get_entities()) |
| 194 | + >>> wt.balance_predators_and_prey() |
| 195 | + >>> len(wt.get_entities()) == entities |
| 196 | + False |
| 197 | + """ |
| 198 | + entities = self.get_entities() |
| 199 | + shuffle(entities) |
| 200 | + |
| 201 | + if len(entities) >= MAX_ENTITIES - MAX_ENTITIES / 10: |
| 202 | + prey = [entity for entity in entities if entity.prey] |
| 203 | + predators = [entity for entity in entities if not entity.prey] |
| 204 | + |
| 205 | + prey_count, predator_count = len(prey), len(predators) |
| 206 | + |
| 207 | + entities_to_purge = ( |
| 208 | + prey[:DELETE_UNBALANCED_ENTITIES] |
| 209 | + if prey_count > predator_count |
| 210 | + else predators[:DELETE_UNBALANCED_ENTITIES] |
| 211 | + ) |
| 212 | + for entity in entities_to_purge: |
| 213 | + self.planet[entity.coords[0]][entity.coords[1]] = None |
| 214 | + |
| 215 | + def get_surrounding_prey(self, entity: Entity) -> list[Entity]: |
| 216 | + """ |
| 217 | + Returns all the prey entities around (N, S, E, W) a predator entity. |
| 218 | +
|
| 219 | + Subtly different to the try_to_move_to_unoccupied square. |
| 220 | +
|
| 221 | + >>> wt = WaTor(WIDTH, HEIGHT) |
| 222 | + >>> wt.set_planet([ |
| 223 | + ... [None, Entity(True, (0, 1)), None], |
| 224 | + ... [None, Entity(False, (1, 1)), None], |
| 225 | + ... [None, Entity(True, (2, 1)), None]]) |
| 226 | + >>> wt.get_surrounding_prey( |
| 227 | + ... Entity(False, (1, 1))) # doctest: +NORMALIZE_WHITESPACE |
| 228 | + [Entity(prey=True, coords=(0, 1), remaining_reproduction_time=5), |
| 229 | + Entity(prey=True, coords=(2, 1), remaining_reproduction_time=5)] |
| 230 | + >>> wt.set_planet([[Entity(False, (0, 0))]]) |
| 231 | + >>> wt.get_surrounding_prey(Entity(False, (0, 0))) |
| 232 | + [] |
| 233 | + >>> wt.set_planet([ |
| 234 | + ... [Entity(True, (0, 0)), Entity(False, (1, 0)), Entity(False, (2, 0))], |
| 235 | + ... [None, Entity(False, (1, 1)), Entity(True, (2, 1))], |
| 236 | + ... [None, None, None]]) |
| 237 | + >>> wt.get_surrounding_prey(Entity(False, (1, 0))) |
| 238 | + [Entity(prey=True, coords=(0, 0), remaining_reproduction_time=5)] |
| 239 | + """ |
| 240 | + row, col = entity.coords |
| 241 | + adjacent: list[tuple[int, int]] = [ |
| 242 | + (row - 1, col), # North |
| 243 | + (row + 1, col), # South |
| 244 | + (row, col - 1), # West |
| 245 | + (row, col + 1), # East |
| 246 | + ] |
| 247 | + |
| 248 | + return [ |
| 249 | + ent |
| 250 | + for r, c in adjacent |
| 251 | + if 0 <= r < self.height |
| 252 | + and 0 <= c < self.width |
| 253 | + and (ent := self.planet[r][c]) is not None |
| 254 | + and ent.prey |
| 255 | + ] |
| 256 | + |
| 257 | + def move_and_reproduce( |
| 258 | + self, entity: Entity, direction_orders: list[Literal["N", "E", "S", "W"]] |
| 259 | + ) -> None: |
| 260 | + """ |
| 261 | + Attempts to move to an unoccupied neighbouring square |
| 262 | + in either of the four directions (North, South, East, West). |
| 263 | + If the move was successful and the remaining_reproduction time is |
| 264 | + equal to 0, then a new prey or predator can also be created |
| 265 | + in the previous square. |
| 266 | +
|
| 267 | + :param direction_orders: Ordered list (like priority queue) depicting |
| 268 | + order to attempt to move. Removes any systematic |
| 269 | + approach of checking neighbouring squares. |
| 270 | +
|
| 271 | + >>> planet = [ |
| 272 | + ... [None, None, None], |
| 273 | + ... [None, Entity(True, coords=(1, 1)), None], |
| 274 | + ... [None, None, None] |
| 275 | + ... ] |
| 276 | + >>> wt = WaTor(WIDTH, HEIGHT) |
| 277 | + >>> wt.set_planet(planet) |
| 278 | + >>> wt.move_and_reproduce(Entity(True, coords=(1, 1)), direction_orders=["N"]) |
| 279 | + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE |
| 280 | + [[None, Entity(prey=True, coords=(0, 1), remaining_reproduction_time=4), None], |
| 281 | + [None, None, None], |
| 282 | + [None, None, None]] |
| 283 | + >>> wt.planet[0][0] = Entity(True, coords=(0, 0)) |
| 284 | + >>> wt.move_and_reproduce(Entity(True, coords=(0, 1)), |
| 285 | + ... direction_orders=["N", "W", "E", "S"]) |
| 286 | + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE |
| 287 | + [[Entity(prey=True, coords=(0, 0), remaining_reproduction_time=5), None, |
| 288 | + Entity(prey=True, coords=(0, 2), remaining_reproduction_time=4)], |
| 289 | + [None, None, None], |
| 290 | + [None, None, None]] |
| 291 | + >>> wt.planet[0][1] = wt.planet[0][2] |
| 292 | + >>> wt.planet[0][2] = None |
| 293 | + >>> wt.move_and_reproduce(Entity(True, coords=(0, 1)), |
| 294 | + ... direction_orders=["N", "W", "S", "E"]) |
| 295 | + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE |
| 296 | + [[Entity(prey=True, coords=(0, 0), remaining_reproduction_time=5), None, None], |
| 297 | + [None, Entity(prey=True, coords=(1, 1), remaining_reproduction_time=4), None], |
| 298 | + [None, None, None]] |
| 299 | +
|
| 300 | + >>> wt = WaTor(WIDTH, HEIGHT) |
| 301 | + >>> reproducable_entity = Entity(False, coords=(0, 1)) |
| 302 | + >>> reproducable_entity.remaining_reproduction_time = 0 |
| 303 | + >>> wt.planet = [[None, reproducable_entity]] |
| 304 | + >>> wt.move_and_reproduce(reproducable_entity, |
| 305 | + ... direction_orders=["N", "W", "S", "E"]) |
| 306 | + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE |
| 307 | + [[Entity(prey=False, coords=(0, 0), |
| 308 | + remaining_reproduction_time=20, energy_value=15), |
| 309 | + Entity(prey=False, coords=(0, 1), remaining_reproduction_time=20, |
| 310 | + energy_value=15)]] |
| 311 | + """ |
| 312 | + row, col = coords = entity.coords |
| 313 | + |
| 314 | + adjacent_squares: dict[Literal["N", "E", "S", "W"], tuple[int, int]] = { |
| 315 | + "N": (row - 1, col), # North |
| 316 | + "S": (row + 1, col), # South |
| 317 | + "W": (row, col - 1), # West |
| 318 | + "E": (row, col + 1), # East |
| 319 | + } |
| 320 | + # Weight adjacent locations |
| 321 | + adjacent: list[tuple[int, int]] = [] |
| 322 | + for order in direction_orders: |
| 323 | + adjacent.append(adjacent_squares[order]) |
| 324 | + |
| 325 | + for r, c in adjacent: |
| 326 | + if ( |
| 327 | + 0 <= r < self.height |
| 328 | + and 0 <= c < self.width |
| 329 | + and self.planet[r][c] is None |
| 330 | + ): |
| 331 | + # Move entity to empty adjacent square |
| 332 | + self.planet[r][c] = entity |
| 333 | + self.planet[row][col] = None |
| 334 | + entity.coords = (r, c) |
| 335 | + break |
| 336 | + |
| 337 | + # (2.) See if it possible to reproduce in previous square |
| 338 | + if coords != entity.coords and entity.remaining_reproduction_time <= 0: |
| 339 | + # Check if the entities on the planet is less than the max limit |
| 340 | + if len(self.get_entities()) < MAX_ENTITIES: |
| 341 | + # Reproduce in previous square |
| 342 | + self.planet[row][col] = Entity(prey=entity.prey, coords=coords) |
| 343 | + entity.reset_reproduction_time() |
| 344 | + else: |
| 345 | + entity.remaining_reproduction_time -= 1 |
| 346 | + |
| 347 | + def perform_prey_actions( |
| 348 | + self, entity: Entity, direction_orders: list[Literal["N", "E", "S", "W"]] |
| 349 | + ) -> None: |
| 350 | + """ |
| 351 | + Performs the actions for a prey entity |
| 352 | +
|
| 353 | + For prey the rules are: |
| 354 | + 1. At each chronon, a prey moves randomly to one of the adjacent unoccupied |
| 355 | + squares. If there are no free squares, no movement takes place. |
| 356 | + 2. Once a prey has survived a certain number of chronons it may reproduce. |
| 357 | + This is done as it moves to a neighbouring square, |
| 358 | + leaving behind a new prey in its old position. |
| 359 | + Its reproduction time is also reset to zero. |
| 360 | +
|
| 361 | + >>> wt = WaTor(WIDTH, HEIGHT) |
| 362 | + >>> reproducable_entity = Entity(True, coords=(0, 1)) |
| 363 | + >>> reproducable_entity.remaining_reproduction_time = 0 |
| 364 | + >>> wt.planet = [[None, reproducable_entity]] |
| 365 | + >>> wt.perform_prey_actions(reproducable_entity, |
| 366 | + ... direction_orders=["N", "W", "S", "E"]) |
| 367 | + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE |
| 368 | + [[Entity(prey=True, coords=(0, 0), remaining_reproduction_time=5), |
| 369 | + Entity(prey=True, coords=(0, 1), remaining_reproduction_time=5)]] |
| 370 | + """ |
| 371 | + self.move_and_reproduce(entity, direction_orders) |
| 372 | + |
| 373 | + def perform_predator_actions( |
| 374 | + self, |
| 375 | + entity: Entity, |
| 376 | + occupied_by_prey_coords: tuple[int, int] | None, |
| 377 | + direction_orders: list[Literal["N", "E", "S", "W"]], |
| 378 | + ) -> None: |
| 379 | + """ |
| 380 | + Performs the actions for a predator entity |
| 381 | +
|
| 382 | + :param occupied_by_prey_coords: Move to this location if there is prey there |
| 383 | +
|
| 384 | + For predators the rules are: |
| 385 | + 1. At each chronon, a predator moves randomly to an adjacent square occupied |
| 386 | + by a prey. If there is none, the predator moves to a random adjacent |
| 387 | + unoccupied square. If there are no free squares, no movement takes place. |
| 388 | + 2. At each chronon, each predator is deprived of a unit of energy. |
| 389 | + 3. Upon reaching zero energy, a predator dies. |
| 390 | + 4. If a predator moves to a square occupied by a prey, |
| 391 | + it eats the prey and earns a certain amount of energy. |
| 392 | + 5. Once a predator has survived a certain number of chronons |
| 393 | + it may reproduce in exactly the same way as the prey. |
| 394 | +
|
| 395 | + >>> wt = WaTor(WIDTH, HEIGHT) |
| 396 | + >>> wt.set_planet([[Entity(True, coords=(0, 0)), Entity(False, coords=(0, 1))]]) |
| 397 | + >>> wt.perform_predator_actions(Entity(False, coords=(0, 1)), (0, 0), []) |
| 398 | + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE |
| 399 | + [[Entity(prey=False, coords=(0, 0), |
| 400 | + remaining_reproduction_time=20, energy_value=19), None]] |
| 401 | + """ |
| 402 | + assert entity.energy_value is not None # [type checking] |
| 403 | + |
| 404 | + # (3.) If the entity has 0 energy, it will die |
| 405 | + if entity.energy_value == 0: |
| 406 | + self.planet[entity.coords[0]][entity.coords[1]] = None |
| 407 | + return |
| 408 | + |
| 409 | + # (1.) Move to entity if possible |
| 410 | + if occupied_by_prey_coords is not None: |
| 411 | + # Kill the prey |
| 412 | + prey = self.planet[occupied_by_prey_coords[0]][occupied_by_prey_coords[1]] |
| 413 | + assert prey is not None |
| 414 | + prey.alive = False |
| 415 | + |
| 416 | + # Move onto prey |
| 417 | + self.planet[occupied_by_prey_coords[0]][occupied_by_prey_coords[1]] = entity |
| 418 | + self.planet[entity.coords[0]][entity.coords[1]] = None |
| 419 | + |
| 420 | + entity.coords = occupied_by_prey_coords |
| 421 | + # (4.) Eats the prey and earns energy |
| 422 | + entity.energy_value += PREDATOR_FOOD_VALUE |
| 423 | + else: |
| 424 | + # (5.) If it has survived the certain number of chronons it will also |
| 425 | + # reproduce in this function |
| 426 | + self.move_and_reproduce(entity, direction_orders) |
| 427 | + |
| 428 | + # (2.) Each chronon, the predator is deprived of a unit of energy |
| 429 | + entity.energy_value -= 1 |
| 430 | + |
| 431 | + def run(self, *, iteration_count: int) -> None: |
| 432 | + """ |
| 433 | + Emulate time passing by looping iteration_count times |
| 434 | +
|
| 435 | + >>> wt = WaTor(WIDTH, HEIGHT) |
| 436 | + >>> wt.run(iteration_count=PREDATOR_INITIAL_ENERGY_VALUE - 1) |
| 437 | + >>> len(list(filter(lambda entity: entity.prey is False, |
| 438 | + ... wt.get_entities()))) >= PREDATOR_INITIAL_COUNT |
| 439 | + True |
| 440 | + """ |
| 441 | + for iter_num in range(iteration_count): |
| 442 | + # Generate list of all entities in order to randomly |
| 443 | + # pop an entity at a time to simulate true randomness |
| 444 | + # This removes the systematic approach of iterating |
| 445 | + # through each entity width by height |
| 446 | + all_entities = self.get_entities() |
| 447 | + |
| 448 | + for __ in range(len(all_entities)): |
| 449 | + entity = all_entities.pop(randint(0, len(all_entities) - 1)) |
| 450 | + if entity.alive is False: |
| 451 | + continue |
| 452 | + |
| 453 | + directions: list[Literal["N", "E", "S", "W"]] = ["N", "E", "S", "W"] |
| 454 | + shuffle(directions) # Randomly shuffle directions |
| 455 | + |
| 456 | + if entity.prey: |
| 457 | + self.perform_prey_actions(entity, directions) |
| 458 | + else: |
| 459 | + # Create list of surrounding prey |
| 460 | + surrounding_prey = self.get_surrounding_prey(entity) |
| 461 | + surrounding_prey_coords = None |
| 462 | + |
| 463 | + if surrounding_prey: |
| 464 | + # Again, randomly shuffle directions |
| 465 | + shuffle(surrounding_prey) |
| 466 | + surrounding_prey_coords = surrounding_prey[0].coords |
| 467 | + |
| 468 | + self.perform_predator_actions( |
| 469 | + entity, surrounding_prey_coords, directions |
| 470 | + ) |
| 471 | + |
| 472 | + # Balance out the predators and prey |
| 473 | + self.balance_predators_and_prey() |
| 474 | + |
| 475 | + if self.time_passed is not None: |
| 476 | + # Call time_passed function for Wa-Tor planet |
| 477 | + # visualisation in a terminal or a graph. |
| 478 | + self.time_passed(self, iter_num) |
| 479 | + |
| 480 | + |
| 481 | +def visualise(wt: WaTor, iter_number: int, *, colour: bool = True) -> None: |
| 482 | + """ |
| 483 | + Visually displays the Wa-Tor planet using |
| 484 | + an ascii code in terminal to clear and re-print |
| 485 | + the Wa-Tor planet at intervals. |
| 486 | +
|
| 487 | + Uses ascii colour codes to colourfully display |
| 488 | + the predators and prey. |
| 489 | +
|
| 490 | + (0x60f197) Prey = # |
| 491 | + (0xfffff) Predator = x |
| 492 | +
|
| 493 | + >>> wt = WaTor(30, 30) |
| 494 | + >>> wt.set_planet([ |
| 495 | + ... [Entity(True, coords=(0, 0)), Entity(False, coords=(0, 1)), None], |
| 496 | + ... [Entity(False, coords=(1, 0)), None, Entity(False, coords=(1, 2))], |
| 497 | + ... [None, Entity(True, coords=(2, 1)), None] |
| 498 | + ... ]) |
| 499 | + >>> visualise(wt, 0, colour=False) # doctest: +NORMALIZE_WHITESPACE |
| 500 | + # x . |
| 501 | + x . x |
| 502 | + . # . |
| 503 | + <BLANKLINE> |
| 504 | + Iteration: 0 | Prey count: 2 | Predator count: 3 | |
| 505 | + """ |
| 506 | + if colour: |
| 507 | + __import__("os").system("") |
| 508 | + print("\x1b[0;0H\x1b[2J\x1b[?25l") |
| 509 | + |
| 510 | + reprint = "\x1b[0;0H" if colour else "" |
| 511 | + ansi_colour_end = "\x1b[0m " if colour else " " |
| 512 | + |
| 513 | + planet = wt.planet |
| 514 | + output = "" |
| 515 | + |
| 516 | + # Iterate over every entity in the planet |
| 517 | + for row in planet: |
| 518 | + for entity in row: |
| 519 | + if entity is None: |
| 520 | + output += " . " |
| 521 | + else: |
| 522 | + if colour is True: |
| 523 | + output += ( |
| 524 | + "\x1b[38;2;96;241;151m" |
| 525 | + if entity.prey |
| 526 | + else "\x1b[38;2;255;255;15m" |
| 527 | + ) |
| 528 | + output += f" {'#' if entity.prey else 'x'}{ansi_colour_end}" |
| 529 | + |
| 530 | + output += "\n" |
| 531 | + |
| 532 | + entities = wt.get_entities() |
| 533 | + prey_count = sum(entity.prey for entity in entities) |
| 534 | + |
| 535 | + print( |
| 536 | + f"{output}\n Iteration: {iter_number} | Prey count: {prey_count} | " |
| 537 | + f"Predator count: {len(entities) - prey_count} | {reprint}" |
| 538 | + ) |
| 539 | + # Block the thread to be able to visualise seeing the algorithm |
| 540 | + sleep(0.05) |
| 541 | + |
| 542 | + |
| 543 | +if __name__ == "__main__": |
| 544 | + import doctest |
| 545 | + |
| 546 | + doctest.testmod() |
| 547 | + |
| 548 | + wt = WaTor(WIDTH, HEIGHT) |
| 549 | + wt.time_passed = visualise |
| 550 | + wt.run(iteration_count=100_000) |
0 commit comments