From 3491a98c5034da041075ff0795463e00bc458a8d Mon Sep 17 00:00:00 2001 From: CaedenPH Date: Fri, 28 Jul 2023 18:58:34 +0300 Subject: [PATCH 1/6] feat(cellular_automata): Create wa-tor algorithm --- cellular_automata/wa_tor.py | 587 ++++++++++++++++++++++++++++++++++++ 1 file changed, 587 insertions(+) create mode 100644 cellular_automata/wa_tor.py diff --git a/cellular_automata/wa_tor.py b/cellular_automata/wa_tor.py new file mode 100644 index 000000000000..f9d19e363442 --- /dev/null +++ b/cellular_automata/wa_tor.py @@ -0,0 +1,587 @@ +""" +Wa-Tor algorithm (1984) + +@ https://en.wikipedia.org/wiki/Wa-Tor +@ https://beltoforion.de/en/wator/ +@ https://beltoforion.de/en/wator/images/wator_medium.webm + +This solution aims to completely remove any systematic approach +to the Wa-Tor planet, and utilise fully random methods. + +The constants are a working set that allows the Wa-Tor planet +to result in one of the three possible results. +""" + +from collections.abc import Callable +from random import randint, shuffle +from time import sleep +from typing import Any, Literal + +WIDTH = 50 # Width of the Wa-Tor planet +HEIGHT = 50 # Height of the Wa-Tor planet + +PREY_INITIAL_COUNT = 30 # The initial number of prey entities +PREY_REPRODUCTION_TIME = 5 # The chronons before reproducing + +PREDATOR_INITIAL_COUNT = 50 # The initial number of predator entities +# The initial energy value of predator entities +PREDATOR_INITIAL_ENERGY_VALUE = 15 +# The energy value provided when consuming prey +PREDATOR_FOOD_VALUE = 5 +PREDATOR_REPRODUCTION_TIME = 20 # The chronons before reproducing + +MAX_ENTITIES = 500 # The max number of organisms on the board +# The number of entities to delete from the unbalanced side +DELETE_UNBALANCED_ENTITIES = 50 + + +class Entity: + """ + Represents an entity (either prey or predator). + + >>> e = Entity(True, coords=(0, 0)) + >>> e.prey + True + >>> e.coords + (0, 0) + >>> e.alive + True + """ + + def __init__(self, prey: bool, coords: tuple[int, int]): + self.prey = prey + # The (row, col) pos of the entity + self.coords = coords + + self.remaining_reproduction_time = ( + PREY_REPRODUCTION_TIME if prey is True else PREDATOR_REPRODUCTION_TIME + ) + self.energy_value = None if prey is True else PREDATOR_INITIAL_ENERGY_VALUE + self.alive = True + + def reset_reproduction_time(self) -> None: + """ + >>> e = Entity(True, coords=(0, 0)) + >>> e.reset_reproduction_time() + >>> e.remaining_reproduction_time == PREY_REPRODUCTION_TIME + True + >>> e = Entity(False, coords=(0, 0)) + >>> e.reset_reproduction_time() + >>> e.remaining_reproduction_time == PREDATOR_REPRODUCTION_TIME + True + """ + self.remaining_reproduction_time = ( + PREY_REPRODUCTION_TIME if self.prey is True else PREDATOR_REPRODUCTION_TIME + ) + + def __repr__(self) -> str: + """ + >>> Entity(prey=True, coords=(1, 1)) + + >>> Entity(prey=False, coords=(2, 1)) + + """ + repr_ = ( + f"entity_type={'prey' if self.prey is True else 'predator'}" + f" coords={self.coords}" + f" remaining_reproduction_time={self.remaining_reproduction_time}" + ) + if self.prey is False: + repr_ += f" energy={self.energy_value}" + return f"<{repr_}>" + + +class WaTor: + """ + Represents the main Wa-Tor algorithm. + + :attr time_passed: A function that is called every time + time passes (a chronon) in order to visually display + the new Wa-Tor planet. The time_passed function can block + using time.sleep to slow the algorithm progression. + + >>> wt = WaTor(10, 15) + >>> wt.width + 10 + >>> wt.height + 15 + >>> len(wt.planet) + 15 + >>> len(wt.planet[0]) + 10 + >>> len(wt.get_entities()) == PREDATOR_INITIAL_COUNT + PREY_INITIAL_COUNT + True + """ + + time_passed: Callable[["WaTor", int], None] | None + + def __init__(self, width: int, height: int) -> None: + self.width = width + self.height = height + self.time_passed = None + + self.planet: list[list[Entity | None]] = [ + [None for _ in range(width)] for _ in range(height) + ] + + # Populate planet with predators and prey randomly + for _ in range(PREY_INITIAL_COUNT): + self.add_entity(True) + for _ in range(PREDATOR_INITIAL_COUNT): + self.add_entity(False) + self.set_planet(self.planet) + + def set_planet(self, planet: list[list[Entity | None]]) -> None: + """ + Ease of access for testing + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> planet = [ + ... [None, None, None], + ... [None, Entity(True, coords=(1, 1)), None] + ... ] + >>> wt.set_planet(planet) + >>> wt.planet == planet + True + >>> wt.width + 3 + >>> wt.height + 2 + """ + self.planet = planet + self.width = len(planet[0]) + self.height = len(planet) + + def add_entity(self, prey: bool) -> None: + """ + Adds an entity, making sure the entity does + not override another entity + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> wt.set_planet([[None, None], [None, None]]) + >>> wt.add_entity(True) + >>> len(wt.get_entities()) + 1 + >>> wt.add_entity(False) + >>> len(wt.get_entities()) + 2 + """ + while True: + row, col = randint(0, self.height - 1), randint(0, self.width - 1) + if self.planet[row][col] is None: + break + self.planet[row][col] = Entity(prey=prey, coords=(row, col)) + + def get_entities(self) -> list[Entity]: + """ + Returns a list of all the entities within the planet. + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> len(wt.get_entities()) == PREDATOR_INITIAL_COUNT + PREY_INITIAL_COUNT + True + """ + start: Any = [] + return sum( + [[entity for entity in column if entity] for column in self.planet], + start=start, + ) + + def balance_predators_and_prey(self) -> None: + """ + Balances predators and preys so that prey + can not dominate the predators, blocking up + space for them to reproduce. + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> for i in range(2000): + ... row, col = i // HEIGHT, i % WIDTH + ... wt.planet[row][col] = Entity(True, coords=(row, col)) + >>> entities = len(wt.get_entities()) + >>> wt.balance_predators_and_prey() + >>> len(wt.get_entities()) == entities + False + """ + entities = self.get_entities() + shuffle(entities) + + if len(entities) >= MAX_ENTITIES - MAX_ENTITIES / 10: + prey = list(filter(lambda m: m.prey is True, entities)) + predators = list(filter(lambda m: m.prey is True, entities)) + + prey_count, predator_count = len(prey), len(predators) + + if prey_count > predator_count: + for entity in prey[:DELETE_UNBALANCED_ENTITIES]: + # Purge the first n entities of the prey + self.planet[entity.coords[0]][entity.coords[1]] = None + else: + for entity in predators[:DELETE_UNBALANCED_ENTITIES]: + # Purge the first n entities of the predators + self.planet[entity.coords[0]][entity.coords[1]] = None + + def get_surrounding_prey(self, entity: Entity) -> list[Entity]: + """ + Returns all the prey entities around (N, S, E, W) a predator entity. + + Subtly different to the try_to_move_to_unoccupied square. + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> wt.set_planet([ + ... [None, Entity(True, (0, 1)), None], + ... [None, Entity(False, (1, 1)), None], + ... [None, Entity(True, (2, 1)), None]]) + >>> wt.get_surrounding_prey( + ... Entity(False, (1, 1))) # doctest: +NORMALIZE_WHITESPACE + [, + ] + >>> wt.set_planet([[Entity(False, (0, 0))]]) + >>> wt.get_surrounding_prey(Entity(False, (0, 0))) + [] + >>> wt.set_planet([ + ... [Entity(True, (0, 0)), Entity(False, (1, 0)), Entity(False, (2, 0))], + ... [None, Entity(False, (1, 1)), Entity(True, (2, 1))], + ... [None, None, None]]) + >>> wt.get_surrounding_prey(Entity(False, (1, 0))) + [] + """ + coords = entity.coords + row, col = coords + surrounding_prey: list[Entity] = [] + + # Go through N, S, E, W with two booleans + # making four different combinations + for i in range(2): + for j in range(2): + vertical = bool(i) + positive = bool(j) + + # North (make sure in bounds) + if vertical is True and positive is True and row - 1 >= 0: + if ( + ent := self.planet[row - 1][col] + ) is not None and ent.prey is True: + surrounding_prey.append(ent) + # South (make sure in bounds) + elif vertical is True and positive is False and self.height > row + 1: + if ( + ent := self.planet[row + 1][col] + ) is not None and ent.prey is True: + surrounding_prey.append(ent) + # East (make sure in bounds) + elif vertical is False and positive is True and self.width > col + 1: + if ( + ent := self.planet[row][col + 1] + ) is not None and ent.prey is True: + surrounding_prey.append(ent) + # South (make sure in bounds) + elif vertical is False and positive is False and col - 1 >= 0: + if ( + ent := self.planet[row][col - 1] + ) is not None and ent.prey is True: + surrounding_prey.append(ent) + return surrounding_prey + + def move_and_reproduce( + self, entity: Entity, direction_orders: list[Literal["N", "E", "S", "W"]] + ) -> None: + """ + Attempts to move to an unoccupied neighbouring square + in either of the four directions (North, South, East, West). + If the move was successful and the remaining_reproduction time is + equal to 0, then a new prey or predator can also be created + in the previous square. + + :param direction_orders: Ordered list (like priority queue) depicting + order to attempt to move. Removes any systematic + approach of checking neighbouring squares. + + >>> planet = [ + ... [None, None, None], + ... [None, Entity(True, coords=(1, 1)), None], + ... [None, None, None] + ... ] + >>> wt = WaTor(WIDTH, HEIGHT) + >>> wt.set_planet(planet) + >>> wt.move_and_reproduce(Entity(True, coords=(1, 1)), direction_orders=["N"]) + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE + [[None, , None], + [None, None, None], + [None, None, None]] + >>> wt.planet[0][0] = Entity(True, coords=(0, 0)) + >>> wt.planet[0][2] = None + >>> wt.move_and_reproduce(Entity(True, coords=(0, 1)), + ... direction_orders=["N", "W", "E", "S"]) + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE + [[, None, + ], + [None, None, None], + [None, None, None]] + >>> wt.planet[0][1] = wt.planet[0][2] + >>> wt.planet[0][2] = None + >>> wt.move_and_reproduce(Entity(True, coords=(0, 1)), + ... direction_orders=["N", "W", "S", "E"]) + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE + [[, None, None], + [None, , None], + [None, None, None]] + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> reproducable_entity = Entity(False, coords=(0, 1)) + >>> reproducable_entity.remaining_reproduction_time = 0 + >>> wt.planet = [[None, reproducable_entity]] + >>> wt.move_and_reproduce(reproducable_entity, + ... direction_orders=["N", "W", "S", "E"]) + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE + [[, + ]] + """ + coords = entity.coords + row, col = coords + + for direction in direction_orders: + # If the direction is North and the northern square + # is within the top bound of the planet + if direction == "N" and row - 1 >= 0: + if self.planet[row - 1][col] is None: + self.planet[row - 1][col] = entity + entity.coords = (row - 1, col) + # If the direction is South and the southern square + # is within the bottom bound of the planet + elif direction == "S" and self.height > row + 1: + if self.planet[row + 1][col] is None: + self.planet[row + 1][col] = entity + entity.coords = (row + 1, col) + # If the direction is East and the eastern square + # is within the right bound of the planet + elif direction == "E" and self.width > col + 1: + if self.planet[row][col + 1] is None: + self.planet[row][col + 1] = entity + entity.coords = (row, col + 1) + # If the direction is West and the western square + # is within the left bound of the planet + elif direction == "W" and col - 1 >= 0: + if self.planet[row][col - 1] is None: + self.planet[row][col - 1] = entity + entity.coords = (row, col - 1) + + # See if move was successful (instead of adding a break) + # to each successful move + if coords != entity.coords: + # Remove the previous location of the entity + self.planet[row][col] = None + break + + # (2.) See if it possible to reproduce in previous square + if coords != entity.coords and entity.remaining_reproduction_time <= 0: + # Check if the entities on the planet is less than the max limit + if len(self.get_entities()) < MAX_ENTITIES: + # Reproduce in previous square + self.planet[row][col] = Entity(prey=entity.prey, coords=coords) + entity.reset_reproduction_time() + else: + entity.remaining_reproduction_time -= 1 + + def perform_prey_actions( + self, entity: Entity, direction_orders: list[Literal["N", "E", "S", "W"]] + ) -> None: + """ + Performs the actions for a prey entity + + For prey the rules are: + 1. At each chronon, a prey moves randomly to one of the adjacent unoccupied + squares. If there are no free squares, no movement takes place. + 2. Once a prey has survived a certain number of chronons it may reproduce. + This is done as it moves to a neighbouring square, + leaving behind a new prey in its old position. + Its reproduction time is also reset to zero. + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> reproducable_entity = Entity(True, coords=(0, 1)) + >>> reproducable_entity.remaining_reproduction_time = 0 + >>> wt.planet = [[None, reproducable_entity]] + >>> wt.perform_prey_actions(reproducable_entity, + ... direction_orders=["N", "W", "S", "E"]) + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE + [[, + ]] + """ + self.move_and_reproduce(entity, direction_orders) + + def perform_predator_actions( + self, + entity: Entity, + occupied_by_prey_coords: tuple[int, int] | None, + direction_orders: list[Literal["N", "E", "S", "W"]], + ) -> None: + """ + Performs the actions for a predator entity + + :param occupied_by_prey_coords: Move to this location if there is prey there + + For predators the rules are: + 1. At each chronon, a predator moves randomly to an adjacent square occupied + by a prey. If there is none, the predator moves to a random adjacent + unoccupied square. If there are no free squares, no movement takes place. + 2. At each chronon, each predator is deprived of a unit of energy. + 3. Upon reaching zero energy, a predator dies. + 4. If a predator moves to a square occupied by a prey, + it eats the prey and earns a certain amount of energy. + 5. Once a predator has survived a certain number of chronons + it may reproduce in exactly the same way as the prey. + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> wt.set_planet([[Entity(True, coords=(0, 0)), Entity(False, coords=(0, 1))]]) + >>> wt.perform_predator_actions(Entity(False, coords=(0, 1)), (0, 0), []) + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE + [[, None]] + """ + assert entity.energy_value is not None # [type checking] + + # (3.) If the entity has 0 energy, it will die + if entity.energy_value == 0: + self.planet[entity.coords[0]][entity.coords[1]] = None + return + + # (1.) Move to entity if possible + if occupied_by_prey_coords is not None: + # Kill the prey + prey = self.planet[occupied_by_prey_coords[0]][occupied_by_prey_coords[1]] + assert prey is not None + prey.alive = False + + # Move onto prey + self.planet[occupied_by_prey_coords[0]][occupied_by_prey_coords[1]] = entity + self.planet[entity.coords[0]][entity.coords[1]] = None + + entity.coords = occupied_by_prey_coords + # (4.) Eats the prey and earns energy + entity.energy_value += PREDATOR_FOOD_VALUE + else: + # (5.) If it has survived the certain number of chronons it will also + # reproduce in this function + self.move_and_reproduce(entity, direction_orders) + + # (2.) Each chronon, the predator is deprived of a unit of energy + entity.energy_value -= 1 + + def run(self, *, iteration_count: int) -> None: + """ + Emulate time passing by looping iteration_count times + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> wt.run(iteration_count=PREDATOR_INITIAL_ENERGY_VALUE - 1) + >>> len(list(filter(lambda m: m.prey is False, + ... wt.get_entities()))) >= PREDATOR_INITIAL_COUNT + True + """ + for iter_num in range(iteration_count): + # Generate list of all entities in order to randomly + # pop an entity at a time to simulate true randomness + # This removes the systematic approach of iterating + # through each entity width by height + all_entities = self.get_entities() + + for __ in range(len(all_entities)): + entity = all_entities.pop(randint(0, len(all_entities) - 1)) + if entity.alive is False: + continue + + directions: list[Literal["N", "E", "S", "W"]] = ["N", "E", "S", "W"] + shuffle(directions) # Randomly shuffle directions + + if entity.prey: + self.perform_prey_actions(entity, directions) + else: + # Create list of surrounding prey + surrounding_prey = self.get_surrounding_prey(entity) + surrounding_prey_coords = None + + if surrounding_prey: + # Again, randomly shuffle directions + shuffle(surrounding_prey) + surrounding_prey_coords = surrounding_prey[0].coords + + self.perform_predator_actions( + entity, surrounding_prey_coords, directions + ) + + # Balance out the predators and prey + self.balance_predators_and_prey() + + if self.time_passed is not None: + # Call time_passed function for Wa-Tor planet + # visualisation in a terminal or a graph. + self.time_passed(self, iter_num) + + +def display_visually(wt: WaTor, iter_number: int, *, colour: bool = True) -> None: + """ + Visually displays the Wa-Tor planet using + an ascii code in terminal to clear and re-print + the Wa-Tor planet at intervals. + + Uses ascii colour codes to colourfully display + the predators and prey. + + (0x60f197) Prey = # + (0xfffff) Predator = x + + >>> wt = WaTor(30, 30) + >>> wt.set_planet([ + ... [Entity(True, coords=(0, 0)), Entity(False, coords=(0, 1)), None], + ... [Entity(False, coords=(1, 0)), None, Entity(False, coords=(1, 2))], + ... [None, Entity(True, coords=(2, 1)), None] + ... ]) + >>> display_visually(wt, 0, colour=False) # doctest: +NORMALIZE_WHITESPACE + # x . + x . x + . # . + + Iteration: 0 | Prey count: 2 | Predator count: 3 | + """ + if colour is True: + __import__("os").system("") + print("\x1b[0;0H\x1b[2J\x1b[?25l") + + reprint = "\x1b[0;0H" if colour is True else "" + ansii_colour_end = "\x1b[0m " if colour is True else " " + + planet = wt.planet + output = "" + + # Iterate over every entity in the planet + for i in range(len(planet)): + for j in range(len(planet[0])): + if (entity := planet[i][j]) is None: + output += " . " + else: + if colour is True: + output += ( + "\x1b[38;2;96;241;151m" + if entity.prey + else "\x1b[38;2;255;255;15m" + ) + output += f" {'#' if entity.prey else 'x'}{ansii_colour_end}" + + output += "\n" + + entities = wt.get_entities() + prey_count = len(list(filter(lambda m: m.prey is True, entities))) + + print( + f"{output}\n Iteration: {iter_number} | Prey count: {prey_count} | " + f"Predator count: {len(entities) - prey_count} | {reprint}" + ) + # Block the thread to be able to visualise seeing the algorithm + sleep(0.05) + + +if __name__ == "__main__": + # import doctest + + # doctest.testmod() + + wt = WaTor(WIDTH, HEIGHT) + wt.time_passed = display_visually + wt.run(iteration_count=100_000) From a0523653ac69917276b1c4434efac2da2dcfec10 Mon Sep 17 00:00:00 2001 From: github-actions <${GITHUB_ACTOR}@users.noreply.github.com> Date: Fri, 28 Jul 2023 16:02:00 +0000 Subject: [PATCH 2/6] updating DIRECTORY.md --- DIRECTORY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/DIRECTORY.md b/DIRECTORY.md index 77938f45011b..3f3a132846ab 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -74,6 +74,7 @@ * [Game Of Life](cellular_automata/game_of_life.py) * [Nagel Schrekenberg](cellular_automata/nagel_schrekenberg.py) * [One Dimensional](cellular_automata/one_dimensional.py) + * [Wa Tor](cellular_automata/wa_tor.py) ## Ciphers * [A1Z26](ciphers/a1z26.py) From f79680b915ab36b3f9e0b1149f47cb1d13594c47 Mon Sep 17 00:00:00 2001 From: CaedenPH Date: Fri, 28 Jul 2023 19:51:48 +0300 Subject: [PATCH 3/6] chore(quality): Implement algo-keeper bot changes --- cellular_automata/wa_tor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cellular_automata/wa_tor.py b/cellular_automata/wa_tor.py index f9d19e363442..34eefc510e66 100644 --- a/cellular_automata/wa_tor.py +++ b/cellular_automata/wa_tor.py @@ -48,7 +48,7 @@ class Entity: True """ - def __init__(self, prey: bool, coords: tuple[int, int]): + def __init__(self, prey: bool, coords: tuple[int, int]) -> None: self.prey = prey # The (row, col) pos of the entity self.coords = coords @@ -205,8 +205,8 @@ def balance_predators_and_prey(self) -> None: shuffle(entities) if len(entities) >= MAX_ENTITIES - MAX_ENTITIES / 10: - prey = list(filter(lambda m: m.prey is True, entities)) - predators = list(filter(lambda m: m.prey is True, entities)) + prey = list(filter(lambda entity: entity.prey is True, entities)) + predators = list(filter(lambda entity: entity.prey is True, entities)) prey_count, predator_count = len(prey), len(predators) @@ -471,7 +471,7 @@ def run(self, *, iteration_count: int) -> None: >>> wt = WaTor(WIDTH, HEIGHT) >>> wt.run(iteration_count=PREDATOR_INITIAL_ENERGY_VALUE - 1) - >>> len(list(filter(lambda m: m.prey is False, + >>> len(list(filter(lambda entity: entity.prey is False, ... wt.get_entities()))) >= PREDATOR_INITIAL_COUNT True """ @@ -567,7 +567,7 @@ def display_visually(wt: WaTor, iter_number: int, *, colour: bool = True) -> Non output += "\n" entities = wt.get_entities() - prey_count = len(list(filter(lambda m: m.prey is True, entities))) + prey_count = len(list(filter(lambda entity: entity.prey is True, entities))) print( f"{output}\n Iteration: {iter_number} | Prey count: {prey_count} | " From 0a192e802144995aa72c36ef8036ef86c186ed8c Mon Sep 17 00:00:00 2001 From: CaedenPH Date: Fri, 28 Jul 2023 19:52:45 +0300 Subject: [PATCH 4/6] build: Fix broken ci --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index acfbc823e77f..2702523d542e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,7 @@ pandas pillow projectq qiskit +qiskit-aer requests rich scikit-fuzzy From 2fe64440b306606a808ea2b563ccc0a5f0b51c54 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Fri, 28 Jul 2023 20:10:23 +0200 Subject: [PATCH 5/6] git rm cellular_automata/wa_tor.py --- cellular_automata/wa_tor.py | 587 ------------------------------------ 1 file changed, 587 deletions(-) delete mode 100644 cellular_automata/wa_tor.py diff --git a/cellular_automata/wa_tor.py b/cellular_automata/wa_tor.py deleted file mode 100644 index 34eefc510e66..000000000000 --- a/cellular_automata/wa_tor.py +++ /dev/null @@ -1,587 +0,0 @@ -""" -Wa-Tor algorithm (1984) - -@ https://en.wikipedia.org/wiki/Wa-Tor -@ https://beltoforion.de/en/wator/ -@ https://beltoforion.de/en/wator/images/wator_medium.webm - -This solution aims to completely remove any systematic approach -to the Wa-Tor planet, and utilise fully random methods. - -The constants are a working set that allows the Wa-Tor planet -to result in one of the three possible results. -""" - -from collections.abc import Callable -from random import randint, shuffle -from time import sleep -from typing import Any, Literal - -WIDTH = 50 # Width of the Wa-Tor planet -HEIGHT = 50 # Height of the Wa-Tor planet - -PREY_INITIAL_COUNT = 30 # The initial number of prey entities -PREY_REPRODUCTION_TIME = 5 # The chronons before reproducing - -PREDATOR_INITIAL_COUNT = 50 # The initial number of predator entities -# The initial energy value of predator entities -PREDATOR_INITIAL_ENERGY_VALUE = 15 -# The energy value provided when consuming prey -PREDATOR_FOOD_VALUE = 5 -PREDATOR_REPRODUCTION_TIME = 20 # The chronons before reproducing - -MAX_ENTITIES = 500 # The max number of organisms on the board -# The number of entities to delete from the unbalanced side -DELETE_UNBALANCED_ENTITIES = 50 - - -class Entity: - """ - Represents an entity (either prey or predator). - - >>> e = Entity(True, coords=(0, 0)) - >>> e.prey - True - >>> e.coords - (0, 0) - >>> e.alive - True - """ - - def __init__(self, prey: bool, coords: tuple[int, int]) -> None: - self.prey = prey - # The (row, col) pos of the entity - self.coords = coords - - self.remaining_reproduction_time = ( - PREY_REPRODUCTION_TIME if prey is True else PREDATOR_REPRODUCTION_TIME - ) - self.energy_value = None if prey is True else PREDATOR_INITIAL_ENERGY_VALUE - self.alive = True - - def reset_reproduction_time(self) -> None: - """ - >>> e = Entity(True, coords=(0, 0)) - >>> e.reset_reproduction_time() - >>> e.remaining_reproduction_time == PREY_REPRODUCTION_TIME - True - >>> e = Entity(False, coords=(0, 0)) - >>> e.reset_reproduction_time() - >>> e.remaining_reproduction_time == PREDATOR_REPRODUCTION_TIME - True - """ - self.remaining_reproduction_time = ( - PREY_REPRODUCTION_TIME if self.prey is True else PREDATOR_REPRODUCTION_TIME - ) - - def __repr__(self) -> str: - """ - >>> Entity(prey=True, coords=(1, 1)) - - >>> Entity(prey=False, coords=(2, 1)) - - """ - repr_ = ( - f"entity_type={'prey' if self.prey is True else 'predator'}" - f" coords={self.coords}" - f" remaining_reproduction_time={self.remaining_reproduction_time}" - ) - if self.prey is False: - repr_ += f" energy={self.energy_value}" - return f"<{repr_}>" - - -class WaTor: - """ - Represents the main Wa-Tor algorithm. - - :attr time_passed: A function that is called every time - time passes (a chronon) in order to visually display - the new Wa-Tor planet. The time_passed function can block - using time.sleep to slow the algorithm progression. - - >>> wt = WaTor(10, 15) - >>> wt.width - 10 - >>> wt.height - 15 - >>> len(wt.planet) - 15 - >>> len(wt.planet[0]) - 10 - >>> len(wt.get_entities()) == PREDATOR_INITIAL_COUNT + PREY_INITIAL_COUNT - True - """ - - time_passed: Callable[["WaTor", int], None] | None - - def __init__(self, width: int, height: int) -> None: - self.width = width - self.height = height - self.time_passed = None - - self.planet: list[list[Entity | None]] = [ - [None for _ in range(width)] for _ in range(height) - ] - - # Populate planet with predators and prey randomly - for _ in range(PREY_INITIAL_COUNT): - self.add_entity(True) - for _ in range(PREDATOR_INITIAL_COUNT): - self.add_entity(False) - self.set_planet(self.planet) - - def set_planet(self, planet: list[list[Entity | None]]) -> None: - """ - Ease of access for testing - - >>> wt = WaTor(WIDTH, HEIGHT) - >>> planet = [ - ... [None, None, None], - ... [None, Entity(True, coords=(1, 1)), None] - ... ] - >>> wt.set_planet(planet) - >>> wt.planet == planet - True - >>> wt.width - 3 - >>> wt.height - 2 - """ - self.planet = planet - self.width = len(planet[0]) - self.height = len(planet) - - def add_entity(self, prey: bool) -> None: - """ - Adds an entity, making sure the entity does - not override another entity - - >>> wt = WaTor(WIDTH, HEIGHT) - >>> wt.set_planet([[None, None], [None, None]]) - >>> wt.add_entity(True) - >>> len(wt.get_entities()) - 1 - >>> wt.add_entity(False) - >>> len(wt.get_entities()) - 2 - """ - while True: - row, col = randint(0, self.height - 1), randint(0, self.width - 1) - if self.planet[row][col] is None: - break - self.planet[row][col] = Entity(prey=prey, coords=(row, col)) - - def get_entities(self) -> list[Entity]: - """ - Returns a list of all the entities within the planet. - - >>> wt = WaTor(WIDTH, HEIGHT) - >>> len(wt.get_entities()) == PREDATOR_INITIAL_COUNT + PREY_INITIAL_COUNT - True - """ - start: Any = [] - return sum( - [[entity for entity in column if entity] for column in self.planet], - start=start, - ) - - def balance_predators_and_prey(self) -> None: - """ - Balances predators and preys so that prey - can not dominate the predators, blocking up - space for them to reproduce. - - >>> wt = WaTor(WIDTH, HEIGHT) - >>> for i in range(2000): - ... row, col = i // HEIGHT, i % WIDTH - ... wt.planet[row][col] = Entity(True, coords=(row, col)) - >>> entities = len(wt.get_entities()) - >>> wt.balance_predators_and_prey() - >>> len(wt.get_entities()) == entities - False - """ - entities = self.get_entities() - shuffle(entities) - - if len(entities) >= MAX_ENTITIES - MAX_ENTITIES / 10: - prey = list(filter(lambda entity: entity.prey is True, entities)) - predators = list(filter(lambda entity: entity.prey is True, entities)) - - prey_count, predator_count = len(prey), len(predators) - - if prey_count > predator_count: - for entity in prey[:DELETE_UNBALANCED_ENTITIES]: - # Purge the first n entities of the prey - self.planet[entity.coords[0]][entity.coords[1]] = None - else: - for entity in predators[:DELETE_UNBALANCED_ENTITIES]: - # Purge the first n entities of the predators - self.planet[entity.coords[0]][entity.coords[1]] = None - - def get_surrounding_prey(self, entity: Entity) -> list[Entity]: - """ - Returns all the prey entities around (N, S, E, W) a predator entity. - - Subtly different to the try_to_move_to_unoccupied square. - - >>> wt = WaTor(WIDTH, HEIGHT) - >>> wt.set_planet([ - ... [None, Entity(True, (0, 1)), None], - ... [None, Entity(False, (1, 1)), None], - ... [None, Entity(True, (2, 1)), None]]) - >>> wt.get_surrounding_prey( - ... Entity(False, (1, 1))) # doctest: +NORMALIZE_WHITESPACE - [, - ] - >>> wt.set_planet([[Entity(False, (0, 0))]]) - >>> wt.get_surrounding_prey(Entity(False, (0, 0))) - [] - >>> wt.set_planet([ - ... [Entity(True, (0, 0)), Entity(False, (1, 0)), Entity(False, (2, 0))], - ... [None, Entity(False, (1, 1)), Entity(True, (2, 1))], - ... [None, None, None]]) - >>> wt.get_surrounding_prey(Entity(False, (1, 0))) - [] - """ - coords = entity.coords - row, col = coords - surrounding_prey: list[Entity] = [] - - # Go through N, S, E, W with two booleans - # making four different combinations - for i in range(2): - for j in range(2): - vertical = bool(i) - positive = bool(j) - - # North (make sure in bounds) - if vertical is True and positive is True and row - 1 >= 0: - if ( - ent := self.planet[row - 1][col] - ) is not None and ent.prey is True: - surrounding_prey.append(ent) - # South (make sure in bounds) - elif vertical is True and positive is False and self.height > row + 1: - if ( - ent := self.planet[row + 1][col] - ) is not None and ent.prey is True: - surrounding_prey.append(ent) - # East (make sure in bounds) - elif vertical is False and positive is True and self.width > col + 1: - if ( - ent := self.planet[row][col + 1] - ) is not None and ent.prey is True: - surrounding_prey.append(ent) - # South (make sure in bounds) - elif vertical is False and positive is False and col - 1 >= 0: - if ( - ent := self.planet[row][col - 1] - ) is not None and ent.prey is True: - surrounding_prey.append(ent) - return surrounding_prey - - def move_and_reproduce( - self, entity: Entity, direction_orders: list[Literal["N", "E", "S", "W"]] - ) -> None: - """ - Attempts to move to an unoccupied neighbouring square - in either of the four directions (North, South, East, West). - If the move was successful and the remaining_reproduction time is - equal to 0, then a new prey or predator can also be created - in the previous square. - - :param direction_orders: Ordered list (like priority queue) depicting - order to attempt to move. Removes any systematic - approach of checking neighbouring squares. - - >>> planet = [ - ... [None, None, None], - ... [None, Entity(True, coords=(1, 1)), None], - ... [None, None, None] - ... ] - >>> wt = WaTor(WIDTH, HEIGHT) - >>> wt.set_planet(planet) - >>> wt.move_and_reproduce(Entity(True, coords=(1, 1)), direction_orders=["N"]) - >>> wt.planet # doctest: +NORMALIZE_WHITESPACE - [[None, , None], - [None, None, None], - [None, None, None]] - >>> wt.planet[0][0] = Entity(True, coords=(0, 0)) - >>> wt.planet[0][2] = None - >>> wt.move_and_reproduce(Entity(True, coords=(0, 1)), - ... direction_orders=["N", "W", "E", "S"]) - >>> wt.planet # doctest: +NORMALIZE_WHITESPACE - [[, None, - ], - [None, None, None], - [None, None, None]] - >>> wt.planet[0][1] = wt.planet[0][2] - >>> wt.planet[0][2] = None - >>> wt.move_and_reproduce(Entity(True, coords=(0, 1)), - ... direction_orders=["N", "W", "S", "E"]) - >>> wt.planet # doctest: +NORMALIZE_WHITESPACE - [[, None, None], - [None, , None], - [None, None, None]] - - >>> wt = WaTor(WIDTH, HEIGHT) - >>> reproducable_entity = Entity(False, coords=(0, 1)) - >>> reproducable_entity.remaining_reproduction_time = 0 - >>> wt.planet = [[None, reproducable_entity]] - >>> wt.move_and_reproduce(reproducable_entity, - ... direction_orders=["N", "W", "S", "E"]) - >>> wt.planet # doctest: +NORMALIZE_WHITESPACE - [[, - ]] - """ - coords = entity.coords - row, col = coords - - for direction in direction_orders: - # If the direction is North and the northern square - # is within the top bound of the planet - if direction == "N" and row - 1 >= 0: - if self.planet[row - 1][col] is None: - self.planet[row - 1][col] = entity - entity.coords = (row - 1, col) - # If the direction is South and the southern square - # is within the bottom bound of the planet - elif direction == "S" and self.height > row + 1: - if self.planet[row + 1][col] is None: - self.planet[row + 1][col] = entity - entity.coords = (row + 1, col) - # If the direction is East and the eastern square - # is within the right bound of the planet - elif direction == "E" and self.width > col + 1: - if self.planet[row][col + 1] is None: - self.planet[row][col + 1] = entity - entity.coords = (row, col + 1) - # If the direction is West and the western square - # is within the left bound of the planet - elif direction == "W" and col - 1 >= 0: - if self.planet[row][col - 1] is None: - self.planet[row][col - 1] = entity - entity.coords = (row, col - 1) - - # See if move was successful (instead of adding a break) - # to each successful move - if coords != entity.coords: - # Remove the previous location of the entity - self.planet[row][col] = None - break - - # (2.) See if it possible to reproduce in previous square - if coords != entity.coords and entity.remaining_reproduction_time <= 0: - # Check if the entities on the planet is less than the max limit - if len(self.get_entities()) < MAX_ENTITIES: - # Reproduce in previous square - self.planet[row][col] = Entity(prey=entity.prey, coords=coords) - entity.reset_reproduction_time() - else: - entity.remaining_reproduction_time -= 1 - - def perform_prey_actions( - self, entity: Entity, direction_orders: list[Literal["N", "E", "S", "W"]] - ) -> None: - """ - Performs the actions for a prey entity - - For prey the rules are: - 1. At each chronon, a prey moves randomly to one of the adjacent unoccupied - squares. If there are no free squares, no movement takes place. - 2. Once a prey has survived a certain number of chronons it may reproduce. - This is done as it moves to a neighbouring square, - leaving behind a new prey in its old position. - Its reproduction time is also reset to zero. - - >>> wt = WaTor(WIDTH, HEIGHT) - >>> reproducable_entity = Entity(True, coords=(0, 1)) - >>> reproducable_entity.remaining_reproduction_time = 0 - >>> wt.planet = [[None, reproducable_entity]] - >>> wt.perform_prey_actions(reproducable_entity, - ... direction_orders=["N", "W", "S", "E"]) - >>> wt.planet # doctest: +NORMALIZE_WHITESPACE - [[, - ]] - """ - self.move_and_reproduce(entity, direction_orders) - - def perform_predator_actions( - self, - entity: Entity, - occupied_by_prey_coords: tuple[int, int] | None, - direction_orders: list[Literal["N", "E", "S", "W"]], - ) -> None: - """ - Performs the actions for a predator entity - - :param occupied_by_prey_coords: Move to this location if there is prey there - - For predators the rules are: - 1. At each chronon, a predator moves randomly to an adjacent square occupied - by a prey. If there is none, the predator moves to a random adjacent - unoccupied square. If there are no free squares, no movement takes place. - 2. At each chronon, each predator is deprived of a unit of energy. - 3. Upon reaching zero energy, a predator dies. - 4. If a predator moves to a square occupied by a prey, - it eats the prey and earns a certain amount of energy. - 5. Once a predator has survived a certain number of chronons - it may reproduce in exactly the same way as the prey. - - >>> wt = WaTor(WIDTH, HEIGHT) - >>> wt.set_planet([[Entity(True, coords=(0, 0)), Entity(False, coords=(0, 1))]]) - >>> wt.perform_predator_actions(Entity(False, coords=(0, 1)), (0, 0), []) - >>> wt.planet # doctest: +NORMALIZE_WHITESPACE - [[, None]] - """ - assert entity.energy_value is not None # [type checking] - - # (3.) If the entity has 0 energy, it will die - if entity.energy_value == 0: - self.planet[entity.coords[0]][entity.coords[1]] = None - return - - # (1.) Move to entity if possible - if occupied_by_prey_coords is not None: - # Kill the prey - prey = self.planet[occupied_by_prey_coords[0]][occupied_by_prey_coords[1]] - assert prey is not None - prey.alive = False - - # Move onto prey - self.planet[occupied_by_prey_coords[0]][occupied_by_prey_coords[1]] = entity - self.planet[entity.coords[0]][entity.coords[1]] = None - - entity.coords = occupied_by_prey_coords - # (4.) Eats the prey and earns energy - entity.energy_value += PREDATOR_FOOD_VALUE - else: - # (5.) If it has survived the certain number of chronons it will also - # reproduce in this function - self.move_and_reproduce(entity, direction_orders) - - # (2.) Each chronon, the predator is deprived of a unit of energy - entity.energy_value -= 1 - - def run(self, *, iteration_count: int) -> None: - """ - Emulate time passing by looping iteration_count times - - >>> wt = WaTor(WIDTH, HEIGHT) - >>> wt.run(iteration_count=PREDATOR_INITIAL_ENERGY_VALUE - 1) - >>> len(list(filter(lambda entity: entity.prey is False, - ... wt.get_entities()))) >= PREDATOR_INITIAL_COUNT - True - """ - for iter_num in range(iteration_count): - # Generate list of all entities in order to randomly - # pop an entity at a time to simulate true randomness - # This removes the systematic approach of iterating - # through each entity width by height - all_entities = self.get_entities() - - for __ in range(len(all_entities)): - entity = all_entities.pop(randint(0, len(all_entities) - 1)) - if entity.alive is False: - continue - - directions: list[Literal["N", "E", "S", "W"]] = ["N", "E", "S", "W"] - shuffle(directions) # Randomly shuffle directions - - if entity.prey: - self.perform_prey_actions(entity, directions) - else: - # Create list of surrounding prey - surrounding_prey = self.get_surrounding_prey(entity) - surrounding_prey_coords = None - - if surrounding_prey: - # Again, randomly shuffle directions - shuffle(surrounding_prey) - surrounding_prey_coords = surrounding_prey[0].coords - - self.perform_predator_actions( - entity, surrounding_prey_coords, directions - ) - - # Balance out the predators and prey - self.balance_predators_and_prey() - - if self.time_passed is not None: - # Call time_passed function for Wa-Tor planet - # visualisation in a terminal or a graph. - self.time_passed(self, iter_num) - - -def display_visually(wt: WaTor, iter_number: int, *, colour: bool = True) -> None: - """ - Visually displays the Wa-Tor planet using - an ascii code in terminal to clear and re-print - the Wa-Tor planet at intervals. - - Uses ascii colour codes to colourfully display - the predators and prey. - - (0x60f197) Prey = # - (0xfffff) Predator = x - - >>> wt = WaTor(30, 30) - >>> wt.set_planet([ - ... [Entity(True, coords=(0, 0)), Entity(False, coords=(0, 1)), None], - ... [Entity(False, coords=(1, 0)), None, Entity(False, coords=(1, 2))], - ... [None, Entity(True, coords=(2, 1)), None] - ... ]) - >>> display_visually(wt, 0, colour=False) # doctest: +NORMALIZE_WHITESPACE - # x . - x . x - . # . - - Iteration: 0 | Prey count: 2 | Predator count: 3 | - """ - if colour is True: - __import__("os").system("") - print("\x1b[0;0H\x1b[2J\x1b[?25l") - - reprint = "\x1b[0;0H" if colour is True else "" - ansii_colour_end = "\x1b[0m " if colour is True else " " - - planet = wt.planet - output = "" - - # Iterate over every entity in the planet - for i in range(len(planet)): - for j in range(len(planet[0])): - if (entity := planet[i][j]) is None: - output += " . " - else: - if colour is True: - output += ( - "\x1b[38;2;96;241;151m" - if entity.prey - else "\x1b[38;2;255;255;15m" - ) - output += f" {'#' if entity.prey else 'x'}{ansii_colour_end}" - - output += "\n" - - entities = wt.get_entities() - prey_count = len(list(filter(lambda entity: entity.prey is True, entities))) - - print( - f"{output}\n Iteration: {iter_number} | Prey count: {prey_count} | " - f"Predator count: {len(entities) - prey_count} | {reprint}" - ) - # Block the thread to be able to visualise seeing the algorithm - sleep(0.05) - - -if __name__ == "__main__": - # import doctest - - # doctest.testmod() - - wt = WaTor(WIDTH, HEIGHT) - wt.time_passed = display_visually - wt.run(iteration_count=100_000) From 3709ad69e86943551e31142db802ec8e8d717ff7 Mon Sep 17 00:00:00 2001 From: github-actions <${GITHUB_ACTOR}@users.noreply.github.com> Date: Fri, 28 Jul 2023 18:11:18 +0000 Subject: [PATCH 6/6] updating DIRECTORY.md --- DIRECTORY.md | 1 - 1 file changed, 1 deletion(-) diff --git a/DIRECTORY.md b/DIRECTORY.md index 3f3a132846ab..77938f45011b 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -74,7 +74,6 @@ * [Game Of Life](cellular_automata/game_of_life.py) * [Nagel Schrekenberg](cellular_automata/nagel_schrekenberg.py) * [One Dimensional](cellular_automata/one_dimensional.py) - * [Wa Tor](cellular_automata/wa_tor.py) ## Ciphers * [A1Z26](ciphers/a1z26.py)