from enum import Enum from collections import namedtuple from math import floor import pygame from dataclasses import dataclass CellStates = Enum("CellStates", ("ALIVE", "DEAD")) #using enum for possible iterations on cellular automata XYPair = namedtuple("XYPair", "x y") class Colors(Enum): RED = (255, 0, 0) GREEN = (0, 255, 0) BLUE = (0, 0, 255) WHITE = (255, 255, 255) BLACK = (0, 0, 0) GRAY = (112, 128, 160) #slate gray @dataclass class Render_Info: screen_dimensions: XYPair = XYPair(1000,1000) padding: XYPair = XYPair(1, 1) background_color = Colors.BLACK.value class Cell: PALETTE_MAP = { CellStates.ALIVE: Colors.GREEN.value, CellStates.DEAD: Colors.GRAY.value, } def calculate_screen_position(self, render_info: Render_Info) -> XYPair: def calculate_spacing(i, w, p): return i * (w + 2*p) + p padding = render_info.padding return XYPair(calculate_spacing(self.position.x, self.dimensions.x, padding.x), calculate_spacing(self.position.y, self.dimensions.y, padding.y)) def __init__(self, position: XYPair, dimensions: XYPair, render_info: Render_Info, state: CellStates=CellStates.DEAD) -> None: self.crowding_number = 0 self.position = position self.dimensions = dimensions self.state = state self.click_state = None self.screen_position = self.calculate_screen_position(render_info) self.image = pygame.Surface(self.dimensions) self.image.fill(Colors.GRAY.value) self.rect = pygame.Rect(self.screen_position.x, self.screen_position.y, self.dimensions.x, self.dimensions.y) def set_state(self, state: CellStates): self.state = state self.image.fill(self.PALETTE_MAP[self.state]) def set_crowding_number(self, n: int): self.crowding_number = n def update_state(self): if self.crowding_number < 2 or self.crowding_number >= 4: self.set_state(CellStates.DEAD) if self.crowding_number == 3: self.set_state(CellStates.ALIVE) def draw(self, screen): screen.blit(self.image, self.rect) class Board: def __init__(self, dimensions: XYPair, evolution_time, render_info: Render_Info): screen_dimenions = render_info.screen_dimensions self.evolution_time = evolution_time self.clock = 0 padding = render_info.padding #calculate cell dimensions once instead of x*y times cell_dimensions = XYPair(screen_dimenions.x/dimensions.x - 2*padding.x, screen_dimenions.y/dimensions.y - 2*padding.y) self.dimensions = dimensions self.matrix = [[Cell(XYPair(x,y), cell_dimensions, render_info) for x in range(dimensions.x)] for y in range(dimensions.y)] def normalize_submatrix_point(self, p: XYPair) -> XYPair: def normalize(x: int, cap: int) -> int: if x < 0: return 0 if x >= cap: return cap-1 return x return XYPair(normalize(p.x, self.dimensions.x), normalize(p.y, self.dimensions.y)) def get_submatrix(self, center: XYPair, extents: XYPair): start_point = self.normalize_submatrix_point(XYPair(center.x - extents.x, center.y - extents.y)) end_point = self.normalize_submatrix_point(XYPair(center.x + extents.x, center.y + extents.y)) return [row[start_point.x:end_point.x+1] for row in self.matrix[start_point.y:end_point.y+1]] def calculate_crowding_number(self, center: XYPair, extents: XYPair): submatrix = self.get_submatrix(center, extents) #flattened_submatrix removes the center point because it does not contribute to the crowding number #if syntax is confusing, it just unrolls the 2d list and removes the cell in the center flattened_submatrix = [cell for row in submatrix for cell in row if cell.position != center] living_cells = map(lambda cell: cell.state == CellStates.ALIVE, flattened_submatrix) return sum(living_cells) def fill_crowding(self): for row in self.matrix: for cell in row: crowding_number = self.calculate_crowding_number(cell.position, XYPair(1,1)) cell.set_crowding_number(crowding_number) def evolve_state(self): #calculate crowding numbers before updating any state, otherwise you'll be unhappy self.fill_crowding() for row in self.matrix: for cell in row: cell.update_state() def set_state(self, p: XYPair, state: CellStates): self.matrix[p.y][p.x].set_state(state) def draw(self, screen): for row in self.matrix: for cell in row: cell.draw(screen) def tick_clock(self, dt): self.clock += dt if self.clock >= self.evolution_time: self.evolve_state() self.clock = 0 def handle_click(self, mouse_pos, render_info): def pixel_to_index(x, w, p): return floor((x - p)/(w + 2*p)) #calculating the board index position is wayyy easier than point colliding #with every cell sprite dimensions = self.matrix[0][0].dimensions x = pixel_to_index(mouse_pos[0], dimensions.x, render_info.padding.x) y = pixel_to_index(mouse_pos[1], dimensions.y, render_info.padding.y) cell = self.matrix[y][x] if not self.click_state: if cell.state == CellStates.ALIVE: self.click_state = CellStates.DEAD else: self.click_state = CellStates.ALIVE self.matrix[y][x].set_state(self.click_state) def reset_click(self): self.click_state = None def reset(self): for row in self.matrix: for cell in row: cell.set_state(CellStates.DEAD) def __repr__(self): lines = [] str_buffer = '' lines.append('-'*(self.dimensions.x+2)) for row in self.matrix: str_buffer += '|' for cell in row: str_buffer += '#' if cell.state == CellStates.ALIVE else ' ' str_buffer += '|' lines.append(str_buffer) str_buffer = '' lines.append('-'*(self.dimensions.x+2)) return '\n'.join(lines) def main(): render_info: Render_Info = Render_Info() screen = pygame.display.set_mode(render_info.screen_dimensions) board = Board(XYPair(50, 50), 100, render_info) pygame.display.set_caption("Conway's Game of Life") clock = pygame.time.Clock() paused = True running = True while running: dt = clock.tick(60) board.draw(screen) pygame.display.flip() if not paused: board.tick_clock(dt) if pygame.mouse.get_pressed()[0]: board.handle_click(pygame.mouse.get_pos(), render_info) else: board.reset_click() for event in pygame.event.get(): if event.type == pygame.KEYDOWN: if event.key == pygame.K_SPACE: paused = not paused if event.key == pygame.K_ESCAPE: board.reset() if event.type == pygame.QUIT: running = False if __name__ == '__main__': main()