diff options
| author | Charles <sircharlesaze@gmail.com> | 2019-08-24 09:55:18 +0200 |
|---|---|---|
| committer | Charles <sircharlesaze@gmail.com> | 2019-08-24 09:55:18 +0200 |
| commit | 92eb91cae76192ba0127f8bde3c78d1b3371a63e (patch) | |
| tree | ff3027cb653cfb31c39eeb6bbb1a1e2f63b9d1cd | |
| download | game_of_life-92eb91cae76192ba0127f8bde3c78d1b3371a63e.tar.gz game_of_life-92eb91cae76192ba0127f8bde3c78d1b3371a63e.tar.bz2 game_of_life-92eb91cae76192ba0127f8bde3c78d1b3371a63e.zip | |
Game of life in Python initial commit
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | gol.py | 143 | ||||
| -rw-r--r-- | graphic.py | 114 | ||||
| -rw-r--r-- | main.py | 56 | ||||
| -rw-r--r-- | setup.py | 8 |
5 files changed, 324 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c53fbe --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__ +patterns/* +!/patterns/.gitkeep @@ -0,0 +1,143 @@ +"""Module that contain the GameOfLife class + +The Game append on an infinite 2D grid, +each node of it can be either alive or dead. + +A dead node become alive if he as 3 alive neighbors, +an alive node become dead if he as less than 2 or more than 3 neighbors, +else they stay in their current state. +""" + +import re +import os +import sys +from time import sleep +from math import inf +from random import random +from itertools import product, count +from functools import reduce +from collections import namedtuple + +from graphic import Graphic + + +class GameOfLife: + """Class of Conway's Game of life""" + + NEIGHBORS_MOD = list(product([-1, 0, 1], repeat=2)) + SIZE = namedtuple('Size', 'w h') + NODE = namedtuple('Node', 'x y') + + def __init__(self, kwargs): + """Initialize the set of alive nodes with the selected options.""" + + self.generation_counter = 0 + self.pattern_name = '' + for key, value in kwargs.items(): + setattr(self, key, value) + + if kwargs['search'] != '': + self._search_pattern(kwargs['search']) + + if self.random_rate != 0.0: + self.size = self.SIZE(100, 100) + self.alive_nodes = { + self.NODE(x, y) for x in range(self.size.h) + for y in range(self.size.w) + if random() < self.random_rate} + else: + self.alive_nodes = { + self.NODE(x, y) for x, row in enumerate( + self._generate_pattern_grid_from_file()) + for y, node in enumerate(row) if node} + + def _search_pattern(self, search): + file_names = os.listdir('./patterns') + match = sorted( + filter(lambda n: re.match(r'.*' + re.escape(search) + r'.*', n), + map(lambda n: n[:-4], file_names)), + key=lambda k: len(k) + ) + for m in match: + print(f'- {m}') + sys.exit() + + def _generate_pattern_grid_from_file(self): + """ Try to read the file with the pattern_name, + and extract a grid of 1's and 0's from it. + """ + + with open(f'./patterns/{self.pattern_file_name}.rle', 'r') as pattern_file: + for line in pattern_file: + if line[0] == '#': + if line[1] == 'N': + self.pattern_name = line[3:-1] + elif line[0] == 'x': + self.size = self.SIZE(*map( + int, + re.search(r'^x = (\d+), y = (\d+)', line).groups())) + break + return [ + [ + 1 if n[-1] == 'o' else 0 + for n in re.findall(r'(\d*[a-z])', line) + for _ in range(1 if len(n) == 1 else int(n[:-1]))] + for line in pattern_file.read().replace('\n', '').split('$')] + + def start(self): + """Iterate over generations and print them.""" + + if self.console_display: + while self.generation_counter <= self.max_gen: + self._console_display() + self.next_generation() + + else: + graphic = Graphic(self) + graphic.main_loop() + + def _console_display(self): + print(f'Generation: {self.generation_counter}\n{self._to_string()}\n') + input('Press Enter') if self.inspect else sleep(self.time_step) + + def next_generation(self): + """Go to the next generation.""" + + alive_nodes_cpy = self.alive_nodes.copy() + for n_pos in self._nodes_to_check(): + # Get the number of alive neighbors + # around a cell to apply the rules on it. + alive_neighbors = reduce( + lambda acc, p: acc + 1 if p in self.alive_nodes else acc, + [ + (x + n_pos.x, y + n_pos.y) + for x, y in self.NEIGHBORS_MOD if (x, y) != (0, 0) + ], 0) + + if n_pos not in self.alive_nodes and alive_neighbors == 3: + alive_nodes_cpy.add(n_pos) + elif n_pos in self.alive_nodes and not 2 <= alive_neighbors <= 3: + alive_nodes_cpy.remove(n_pos) + + self.alive_nodes = alive_nodes_cpy + self.generation_counter += 1 + + def _nodes_to_check(self): + """Return all the node to check at some generation""" + + return {self.NODE(x + n_pos.x, y + n_pos.y) + for x, y in self.NEIGHBORS_MOD + for n_pos in self.alive_nodes} + + def _to_string(self): + """Convert a grid of positions in a string. + Use the range of self width and height + and the size of the pattern + """ + + return '\n'.join( + [''.join([ + (self.alive_node_repr + if (x, y) in self.alive_nodes else self.dead_node_repr) + for y in range(-28, 32)]) + for x in range(-13, 17)]) diff --git a/graphic.py b/graphic.py new file mode 100644 index 0000000..5fac320 --- /dev/null +++ b/graphic.py @@ -0,0 +1,114 @@ +import os +from time import sleep, time +from threading import Thread + +import pygame as pg +from pygame.locals import * + + +RESOLUTION = (720, 520) +BLACK_COLOR = (0, 0, 0) +WHITE_COLOR = (255, 255, 255) +DECAL_LENGTH = 20 +FPS = 30 + + +class Graphic: + def __init__(self, gol): + self.gol = gol + self.running = True + self.key_event_occured = False + self.inspect_next = False + self.full_screen = False + self.x_decal = 0 + self.y_decal = 0 + self.zoom_level = 0 + self.time_next_draw = time() + + @property + def square_size(self): + return self.gol.square_size + self.zoom_level + + def main_loop(self): + os.environ['SDL_VIDEO_CENTERED'] = '1' + pg.init() + # display_info = pg.display.Info() + # self.screen_size = (display_info.current_w, display_info.current_h) + # print(RESOLUTION == self.screen_size) + pg.display.set_caption('Game of life') + self.window = pg.display.set_mode(RESOLUTION, RESIZABLE | DOUBLEBUF) + self.sans_font = pg.font.SysFont('sans', 15) + clock = pg.time.Clock() + + while self.running: + clock.tick(FPS) + self.event_handler() + self.update() + pg.display.flip() + + def update(self): + if not self.gol.inspect: + if not time() >= self.time_next_draw: + return + self.time_next_draw = time() + self.gol.time_step + + if self.gol.inspect and not self.key_event_occured: + return + self.window.fill(BLACK_COLOR) + for node in self.gol.alive_nodes: + node_rect = pg.Rect(10 + self.x_decal + (node.x + 1) * self.square_size, + 45 + self.y_decal + (node.y + 1) * self.square_size, + self.square_size, self.square_size) + pg.draw.rect(self.window, WHITE_COLOR, node_rect) + + gen_text = self.sans_font.render(str(self.gol.generation_counter), + True, WHITE_COLOR, BLACK_COLOR) + self.window.blit(gen_text, (10, 35)) + + gen_text = self.sans_font.render(str(self.gol.pattern_name), + True, WHITE_COLOR, BLACK_COLOR) + self.window.blit(gen_text, (10, 10)) + if (self.gol.inspect and self.inspect_next) or not self.gol.inspect: + self.gol.next_generation() + if self.gol.generation_counter > self.gol.max_gen: + self.running = False + self.key_event_occured = False + self.inspect_next = False + + def event_handler(self): + for event in pg.event.get(): + if event.type == QUIT: + self.running = False + elif event.type == KEYDOWN: + self.key_event_occured = True + # trigger next step in inspect mode + if event.key == K_RETURN or event.key == K_SPACE: + self.inspect_next = True + # quit + elif event.key == K_q: + self.running = False + # toggle inspect mode + elif event.key == K_i: + self.gol.inspect = not self.gol.inspect + # elif event.key == K_F11 or (event.key == K_ESCAPE and self.full_screen): + # pg.display.toggle_fullscreen() + # if self.window.get_flags() & FULLSCREEN: + # pg.display.set_mode(RESOLUTION, RESIZABLE | DOUBLEBUF) + # else: + # pg.display.set_mode(RESOLUTION, RESIZABLE | DOUBLEBUF | FULLSCREEN) + # view move + elif event.key == K_UP: + self.y_decal += DECAL_LENGTH + elif event.key == K_DOWN: + self.y_decal -= DECAL_LENGTH + elif event.key == K_LEFT: + self.x_decal += DECAL_LENGTH + elif event.key == K_RIGHT: + self.x_decal -= DECAL_LENGTH + # zoom + elif event.key == K_MINUS: + self.zoom_level -= 1 + elif event.key == K_EQUALS: + self.zoom_level += 1 + elif event.type == VIDEORESIZE: + pg.display.set_mode(event.size, RESIZABLE | DOUBLEBUF) @@ -0,0 +1,56 @@ +"""Main Script + +Command line interface for the Game of life. +""" + +import sys +import argparse +from math import inf + +from gol import GameOfLife + + +def parse_args(): + """Parse and return the necessary arguments.""" + + parser = argparse.ArgumentParser( + prog='A Random Game of Life', + description="Conway's Game of Life in console") + parser.add_argument('-p', '--pattern', + help='initial pattern name', + dest='pattern_file_name', default='glider') + parser.add_argument('-r', '--random-rate', + help='generation of random pattern with some rate', + type=float, default=0.0) + parser.add_argument('-t', '--time-step', + help='time between each step (seconds)', + type=float, + default=0.2) + parser.add_argument('-m', '--max-gen', + help='maximum number of generation to execute', + type=int, default=inf) + parser.add_argument('-i', '--inspect', + help='pause between each step', + action='store_true') + parser.add_argument('-s', '--search', + help="search a pattern in 'patterns/'", + default='') + parser.add_argument('--square-size', + help='size of the representation square', + type=int, default=5) + parser.add_argument('--console-display', + help='console representation', + action='store_true') + parser.add_argument('--alive-node-repr', + help='representation of an alive cell (one char)', + default='O') + parser.add_argument('--dead-node-repr', + help='representation of a dead cell (one char)', + default='.') + + return vars(parser.parse_args(sys.argv[1:])) + + +if __name__ == '__main__': + gol = GameOfLife(parse_args()) + gol.start() diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..62f59d8 --- /dev/null +++ b/setup.py @@ -0,0 +1,8 @@ +from cx_Freeze import setup, Executable + +setup( + name='Game of life', + version='0.1', + description='Implement the game of life with a list of pattern available', + executables=[Executable('main.py')] +) |
