diff --git a/game/main.py b/game/main.py index 8059ed1..114986d 100644 --- a/game/main.py +++ b/game/main.py @@ -1,5 +1,8 @@ +from typing import Annotated + import typer +from game.utils.cli import parse_multiple_value_argument_from_str from game.utils.map import Map from game.utils.tui import run @@ -8,27 +11,50 @@ @app.callback(invoke_without_command=True) def main( - ctx: typer.Context, - map_grid: str = typer.Option( - None, - "--map-grid", - "-m", - help="Load a map from a string representation", - ), + map_grid: Annotated[ + str, + typer.Option( + help="Load a map from a string representation", + ), + ] = None, + file_path: Annotated[ + str, + typer.Option( + help="Load a map from a file", + ), + ] = None, + survival_condition: Annotated[ + str, + typer.Option( + help="Survival condition for the game, e.g., [2, 3]", + ), + ] = None, + birth_condition: Annotated[ + str, + typer.Option( + help="Birth condition for the game, e.g., [3]", + ), + ] = None, + neighbouring_condition: Annotated[ + int, + typer.Option( + help="Number of neighbouring cells to consider for the game", + ), + ] = 1, ): - if ctx.invoked_subcommand is not None: - return - game_map = Map() - if map_grid is not None: - game_map.load_from_str(map_grid) - run(game_map) + if survival_condition is not None: + game_map.survival_condition = parse_multiple_value_argument_from_str(survival_condition) + if birth_condition is not None: + game_map.birth_condition = parse_multiple_value_argument_from_str(birth_condition) + if neighbouring_condition is not None: + game_map.neighbouring_condition = neighbouring_condition -@app.command() -def from_file(file_path: str): - game_map = Map() - game_map.load_from_file(file_path) + if file_path is not None: + game_map.load_from_file(file_path) + elif map_grid is not None: + game_map.load_from_str(map_grid) run(game_map) diff --git a/game/tests/cli/__init__.py b/game/tests/test_cli/__init__.py similarity index 100% rename from game/tests/cli/__init__.py rename to game/tests/test_cli/__init__.py diff --git a/game/tests/cli/expected_display.txt b/game/tests/test_cli/expected_display.txt similarity index 100% rename from game/tests/cli/expected_display.txt rename to game/tests/test_cli/expected_display.txt diff --git a/game/tests/cli/cli.py b/game/tests/test_cli/test_cli.py similarity index 65% rename from game/tests/cli/cli.py rename to game/tests/test_cli/test_cli.py index a60ab12..fe27f2f 100644 --- a/game/tests/cli/cli.py +++ b/game/tests/test_cli/test_cli.py @@ -1,3 +1,4 @@ +from game.utils.cli import parse_multiple_value_argument_from_str from game.utils.map import Map from game.utils.tui import prepare_display @@ -17,7 +18,7 @@ def test_from_arg(): def test_from_file(): game_map = Map() - game_map.load_from_file("game/tests/test_map.txt") + game_map.load_from_file("game/tests/test_cli/test_map.txt") assert game_map.map == [ [False for _ in range(5)], @@ -33,17 +34,23 @@ def test_from_arg_and_from_file_are_identical(): game_map_from_arg.load_from_str("..... .### # ....# .") game_map_from_file = Map() - game_map_from_file.load_from_file("game/tests/cli/test_map.txt") + game_map_from_file.load_from_file("game/tests/test_cli/test_map.txt") assert game_map_from_arg.map == game_map_from_file.map def test_tui(): game_map = Map() - game_map.load_from_file("game/tests/cli/test_map.txt") + game_map.load_from_file("game/tests/test_cli/test_map.txt") display = prepare_display(game_map, state={"paused": False}) - expected_display = open("game/tests/cli/expected_display.txt").read() + expected_display = open("game/tests/test_cli/expected_display.txt").read() assert display == expected_display + + +def test_parsing_multiple_value_argument(): + assert parse_multiple_value_argument_from_str("2, 3") == [2, 3] + assert parse_multiple_value_argument_from_str("3") == [3] + assert parse_multiple_value_argument_from_str("") == [] diff --git a/game/tests/cli/test_map.txt b/game/tests/test_cli/test_map.txt similarity index 100% rename from game/tests/cli/test_map.txt rename to game/tests/test_cli/test_map.txt diff --git a/game/tests/map.py b/game/tests/test_map.py similarity index 79% rename from game/tests/map.py rename to game/tests/test_map.py index f86234d..c2dcede 100644 --- a/game/tests/map.py +++ b/game/tests/test_map.py @@ -10,6 +10,14 @@ def test_neighbour_count(): assert neighbours == excepted_neighbours +def test_neighbour_count_condition_2(): + excepted_neighbours = 3 + game_map = Map() + + neighbours = game_map.count_neighbours(1, 2) + assert neighbours == excepted_neighbours + + def test_neighbour_count_fail(): excepted_neighbours = 1 game_map = Map() diff --git a/game/utils/cli.py b/game/utils/cli.py new file mode 100644 index 0000000..9c75af6 --- /dev/null +++ b/game/utils/cli.py @@ -0,0 +1,5 @@ +def parse_multiple_value_argument_from_str(argument_str: str) -> list[int]: + if not argument_str: + return [] + + return [int(x) for x in argument_str.split(",") if x.strip().isdigit()] diff --git a/game/utils/map.py b/game/utils/map.py index 2c8f862..885aba5 100644 --- a/game/utils/map.py +++ b/game/utils/map.py @@ -3,9 +3,9 @@ class AbstractMap: Abstract class for the map. """ - population = int - generation = int - map = list[list[bool]] + population: int + generation: int + map: list[list[bool]] def __init__(self, game_map: list[list[bool]] = None): if game_map is None: @@ -49,7 +49,7 @@ def count_neighbours(self, x, y) -> int: try: self.map[y][x] except IndexError: - raise IndexError("Invalid coordinates.") from IndexError + raise IndexError("Invalid coordinates.") from None for i in range(y - 1, y + 2): if 0 <= i < len(self.map): @@ -65,15 +65,17 @@ def next_generation(self): class Map(AbstractMap): - birth_condition = (list[int],) - survival_condition = (list[int],) - number_of_rows = int - number_of_columns = int + birth_condition: list[int] + survival_condition: list[int] + neighbouring_condition: int + number_of_rows: int + number_of_columns: int def __init__(self, game_map: list[list[bool]] = None): super().__init__(game_map) self.birth_condition = [3] self.survival_condition = [2, 3] + self.neighbouring_condition = 1 self.number_of_rows = len(self.map) self.number_of_columns = len(self.map[0]) @@ -136,3 +138,26 @@ def next_generation(self): self.map = new_map self.generation += 1 self.population = population + + def count_neighbours(self, x, y) -> int | None: + neighbours = 0 + + if len(self.map) == 1: + if len(self.map[y]) == 1: + return neighbours + + try: + self.map[y][x] + except IndexError: + raise IndexError("Invalid coordinates.") from None + + for neighbouring_y in range(y - self.neighbouring_condition, y + (self.neighbouring_condition + 1)): + if 0 <= neighbouring_y < len(self.map): + for neighbouring_x in range(x - self.neighbouring_condition, x + (self.neighbouring_condition + 1)): + if 0 <= neighbouring_x < len(self.map[neighbouring_y]) and ( + neighbouring_y != y or neighbouring_x != x + ): + if self.map[neighbouring_y][neighbouring_x]: + neighbours += 1 + + return neighbours diff --git a/game/utils/tui.py b/game/utils/tui.py index 73f0d22..ce7b6ac 100644 --- a/game/utils/tui.py +++ b/game/utils/tui.py @@ -2,7 +2,6 @@ from copy import deepcopy from time import sleep -from pynput import keyboard from rich.console import Console from rich.live import Live @@ -26,6 +25,10 @@ def prepare_display(game_map: Map, state: dict) -> str: def detect_key_input(state: dict, lock): + from pynput import ( + keyboard, # todo: fix this, this is placed here to avoid errors when running tests on github actions + ) + def on_press(key): try: if key.char == "p": diff --git a/pyproject.toml b/pyproject.toml index 4dfffd3..8964932 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ packages = ["game"] test = ["pytest"] [tool.pytest.ini_options] -testpaths = ["game/tests/*"] +testpaths = ["game/tests"] [project.scripts] game = "game.main:app" diff --git a/uv.lock b/uv.lock index 1dd0ced..79c151f 100644 --- a/uv.lock +++ b/uv.lock @@ -58,7 +58,7 @@ wheels = [ [[package]] name = "gameoflifepython" -version = "0.1.0" +version = "1.0.0" source = { editable = "." } dependencies = [ { name = "pre-commit" },