aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--gol.py143
-rw-r--r--graphic.py114
-rw-r--r--main.py56
-rw-r--r--setup.py8
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
diff --git a/gol.py b/gol.py
new file mode 100644
index 0000000..a4d5558
--- /dev/null
+++ b/gol.py
@@ -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)
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..cd30014
--- /dev/null
+++ b/main.py
@@ -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')]
+)