From 8902295b058ad4bbdca3ebf65b190516bddacd1b Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Fri, 20 Oct 2023 17:11:08 +0200 Subject: [PATCH] Game of Life source code --- game-of-life-python/README.md | 36 +++++ .../source_code_final/pyproject.toml | 19 +++ .../source_code_final/rplife/__init__.py | 1 + .../source_code_final/rplife/__main__.py | 25 +++ .../source_code_final/rplife/cli.py | 50 ++++++ .../source_code_final/rplife/grid.py | 50 ++++++ .../source_code_final/rplife/patterns.py | 34 +++++ .../source_code_final/rplife/patterns.toml | 142 ++++++++++++++++++ .../source_code_final/rplife/views.py | 35 +++++ 9 files changed, 392 insertions(+) create mode 100755 game-of-life-python/README.md create mode 100644 game-of-life-python/source_code_final/pyproject.toml create mode 100755 game-of-life-python/source_code_final/rplife/__init__.py create mode 100644 game-of-life-python/source_code_final/rplife/__main__.py create mode 100644 game-of-life-python/source_code_final/rplife/cli.py create mode 100755 game-of-life-python/source_code_final/rplife/grid.py create mode 100644 game-of-life-python/source_code_final/rplife/patterns.py create mode 100644 game-of-life-python/source_code_final/rplife/patterns.toml create mode 100644 game-of-life-python/source_code_final/rplife/views.py diff --git a/game-of-life-python/README.md b/game-of-life-python/README.md new file mode 100755 index 0000000000..50482b2f5f --- /dev/null +++ b/game-of-life-python/README.md @@ -0,0 +1,36 @@ +# `rplife` + +Conway's Game of Life in your terminal. + +## Installation + +1. Create and activate a Python virtual environment + +```sh +$ python -m venv ./venv +$ source venv/bin/activate +(venv) $ +``` + +2. Install the `rplife` in editable mode + +```sh +(venv) $ cd rplife +(venv) $ pip install -e . +``` + +## Execution + +To execute `rplife`, go ahead and run the following command: + +```sh +(venv) $ rplife -a +``` + +## Author + +Real Python - Email: office@realpython.com + +## License + +Distributed under the MIT license. See `LICENSE` for more information. \ No newline at end of file diff --git a/game-of-life-python/source_code_final/pyproject.toml b/game-of-life-python/source_code_final/pyproject.toml new file mode 100644 index 0000000000..dbbb1637a4 --- /dev/null +++ b/game-of-life-python/source_code_final/pyproject.toml @@ -0,0 +1,19 @@ +[build-system] +requires = ["setuptools>=64.0.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "rplife" +dynamic = ["version"] +description = "Conway's Game of Life in your terminal" +readme = "README.md" +authors = [{ name = "Real Python", email = "info@realpython.com" }] +dependencies = [ + 'tomli; python_version < "3.11"', +] + +[project.scripts] +rplife = "rplife.__main__:main" + +[tool.setuptools.dynamic] +version = {attr = "rplife.__version__"} \ No newline at end of file diff --git a/game-of-life-python/source_code_final/rplife/__init__.py b/game-of-life-python/source_code_final/rplife/__init__.py new file mode 100755 index 0000000000..5becc17c04 --- /dev/null +++ b/game-of-life-python/source_code_final/rplife/__init__.py @@ -0,0 +1 @@ +__version__ = "1.0.0" diff --git a/game-of-life-python/source_code_final/rplife/__main__.py b/game-of-life-python/source_code_final/rplife/__main__.py new file mode 100644 index 0000000000..8c61fb0040 --- /dev/null +++ b/game-of-life-python/source_code_final/rplife/__main__.py @@ -0,0 +1,25 @@ +import sys + +from rplife import patterns, views +from rplife.cli import get_command_line_args + + +def main(): + args = get_command_line_args() + View = getattr(views, args.view) + if args.all: + for pattern in patterns.get_all_patterns(): + _show_pattern(View, pattern, args) + else: + _show_pattern(View, patterns.get_pattern(name=args.pattern), args) + + +def _show_pattern(View, pattern, args): + try: + View(pattern=pattern, gen=args.gen, framerate=args.fps).show() + except Exception as error: + print(error, file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/game-of-life-python/source_code_final/rplife/cli.py b/game-of-life-python/source_code_final/rplife/cli.py new file mode 100644 index 0000000000..baf8ec1415 --- /dev/null +++ b/game-of-life-python/source_code_final/rplife/cli.py @@ -0,0 +1,50 @@ +import argparse + +from rplife import __version__, patterns, views + + +def get_command_line_args(): + parser = argparse.ArgumentParser( + prog="rplife", + description="Conway's Game of Life in your terminal", + ) + parser.add_argument( + "--version", action="version", version=f"%(prog)s v{__version__}" + ) + parser.add_argument( + "-p", + "--pattern", + choices=[pat.name for pat in patterns.get_all_patterns()], + default="Blinker", + help="take a pattern for the Game of Life (default: %(default)s)", + ) + parser.add_argument( + "-a", + "--all", + action="store_true", + help="show all available patterns in a sequence", + ) + parser.add_argument( + "-g", + "--gen", + metavar="NUM_GENERATIONS", + type=int, + default=10, + help="number of generations (default: %(default)s)", + ) + parser.add_argument( + "-v", + "--view", + choices=views.__all__, + default="CursesView", + help="display the life grid in a specific view (default: %(default)s)", + ) + parser.add_argument( + "-f", + "--fps", + metavar="FRAMES_PER_SECOND", + type=int, + default=7, + help="frames per second (default: %(default)s)", + ) + return parser.parse_args() diff --git a/game-of-life-python/source_code_final/rplife/grid.py b/game-of-life-python/source_code_final/rplife/grid.py new file mode 100755 index 0000000000..71f0516d25 --- /dev/null +++ b/game-of-life-python/source_code_final/rplife/grid.py @@ -0,0 +1,50 @@ +import collections + +ALIVE = "♥" +DEAD = "‧" + + +class LifeGrid: + def __init__(self, pattern): + self.pattern = pattern + + def evolve(self): + neighbors = ( + (-1, -1), + (-1, 0), + (-1, 1), + (0, -1), + (0, 1), + (1, -1), + (1, 0), + (1, 1), + ) + num_neighbors = collections.defaultdict(int) + for row, col in self.pattern.alive_cells: + for drow, dcol in neighbors: + num_neighbors[(row + drow, col + dcol)] += 1 + + stay_alive = { + cell for cell, num in num_neighbors.items() if num in {2, 3} + } & self.pattern.alive_cells + come_alive = { + cell for cell, num in num_neighbors.items() if num == 3 + } - self.pattern.alive_cells + + self.pattern.alive_cells = stay_alive | come_alive + + def as_string(self, bbox): + start_col, start_row, end_col, end_row = bbox + display = [self.pattern.name.center(2 * (end_col - start_col))] + for row in range(start_row, end_row): + display_row = [ + ALIVE if (row, col) in self.pattern.alive_cells else DEAD + for col in range(start_col, end_col) + ] + display.append(" ".join(display_row)) + return "\n ".join(display) + + def __str__(self): + return ( + f"{self.pattern.name}:\nAlive cells -> {self.pattern.alive_cells}" + ) diff --git a/game-of-life-python/source_code_final/rplife/patterns.py b/game-of-life-python/source_code_final/rplife/patterns.py new file mode 100644 index 0000000000..78f8eaf260 --- /dev/null +++ b/game-of-life-python/source_code_final/rplife/patterns.py @@ -0,0 +1,34 @@ +from dataclasses import dataclass +from pathlib import Path + +try: + import tomllib as toml +except ImportError: + import tomli as toml + +PATTERNS_FILE = Path(__file__).parent / "patterns.toml" + + +@dataclass +class Pattern: + name: str + alive_cells: set[tuple[int, int]] + + @classmethod + def from_toml(cls, name, toml_data): + return cls( + name, + alive_cells={tuple(cell) for cell in toml_data["alive_cells"]}, + ) + + +def get_pattern(name, file_name=PATTERNS_FILE): + data = toml.loads(file_name.read_text(encoding="utf-8")) + return Pattern.from_toml(name, toml_data=data[name]) + + +def get_all_patterns(file_name=PATTERNS_FILE): + data = toml.loads(file_name.read_text(encoding="utf-8")) + return [ + Pattern.from_toml(name, toml_data) for name, toml_data in data.items() + ] diff --git a/game-of-life-python/source_code_final/rplife/patterns.toml b/game-of-life-python/source_code_final/rplife/patterns.toml new file mode 100644 index 0000000000..e9d01a497c --- /dev/null +++ b/game-of-life-python/source_code_final/rplife/patterns.toml @@ -0,0 +1,142 @@ +["Blinker"] +alive_cells = [[2, 1], [2, 2], [2, 3]] + +["Toad"] +alive_cells = [[2, 2], [2, 3], [2, 4], [3, 1], [3, 2], [3, 3]] + +["Beacon"] +alive_cells = [[1, 1], [1, 2], [2, 1], [4, 3], [4, 4], [3, 4]] + +["Pulsar"] +alive_cells = [ + [2, 4], + [2, 5], + [2, 6], + [2, 10], + [2, 11], + [2, 12], + [4, 2], + [5, 2], + [6, 2], + [4, 7], + [5, 7], + [6, 7], + [4, 9], + [5, 9], + [6, 9], + [4, 14], + [5, 14], + [6, 14], + [7, 4], + [7, 5], + [7, 6], + [7, 10], + [7, 11], + [7, 12], + [9, 4], + [9, 5], + [9, 6], + [9, 10], + [9, 11], + [9, 12], + [10, 2], + [11, 2], + [12, 2], + [10, 7], + [11, 7], + [12, 7], + [10, 9], + [11, 9], + [12, 9], + [10, 14], + [11, 14], + [12, 14], + [14, 4], + [14, 5], + [14, 6], + [14, 10], + [14, 11], + [14, 12] +] + +["Penta Decathlon"] +alive_cells = [ + [5, 4], + [6, 4], + [7, 4], + [8, 4], + [9, 4], + [10, 4], + [11, 4], + [12, 4], + [5, 5], + [7, 5], + [8, 5], + [9, 5], + [10, 5], + [12, 5], + [5, 6], + [6, 6], + [7, 6], + [8, 6], + [9, 6], + [10, 6], + [11, 6], + [12, 6] +] + +["Glider"] +alive_cells = [[0, 2], [1, 0], [1, 2], [2, 1], [2, 2]] + +["Glider Gun"] +alive_cells = [ + [0, 24], + [1, 22], + [1, 24], + [2, 12], + [2, 13], + [2, 20], + [2, 21], + [2, 34], + [2, 35], + [3, 11], + [3, 15], + [3, 20], + [3, 21], + [3, 34], + [3, 35], + [4, 0], + [4, 1], + [4, 10], + [4, 16], + [4, 20], + [4, 21], + [5, 0], + [5, 1], + [5, 10], + [5, 14], + [5, 16], + [5, 17], + [5, 22], + [5, 24], + [6, 10], + [6, 16], + [6, 24], + [7, 11], + [7, 15], + [8, 12], + [8, 13] +] + +["Bunnies"] +alive_cells = [ + [10, 10], + [10, 16], + [11, 12], + [11, 16], + [12, 12], + [12, 15], + [12, 17], + [13, 11], + [13, 13] +] \ No newline at end of file diff --git a/game-of-life-python/source_code_final/rplife/views.py b/game-of-life-python/source_code_final/rplife/views.py new file mode 100644 index 0000000000..f4a05eee3a --- /dev/null +++ b/game-of-life-python/source_code_final/rplife/views.py @@ -0,0 +1,35 @@ +import curses +from time import sleep + +from rplife import grid + +__all__ = ["CursesView"] + + +class CursesView: + def __init__(self, pattern, gen=10, framerate=7, bbox=(0, 0, 40, 20)): + self.pattern = pattern + self.gen = gen + self.framerate = framerate + self.bbox = bbox + + def show(self): + curses.wrapper(self._draw) + + def _draw(self, screen): + current_grid = grid.LifeGrid(self.pattern) + curses.curs_set(0) + screen.clear() + + try: + screen.addstr(0, 0, current_grid.as_string(self.bbox)) + except curses.error: + raise ValueError( + f"Error: terminal too small for pattern '{self.pattern.name}'" + ) + + for _ in range(self.gen): + current_grid.evolve() + screen.addstr(0, 0, current_grid.as_string(self.bbox)) + screen.refresh() + sleep(1 / self.framerate)