commit 37f1bd6d1a21a8ee2d57cbc6ed81f13b3e42b3f4 Author: = <=> Date: Wed Mar 20 17:35:07 2024 -0500 initial commit diff --git a/conway.py b/conway.py new file mode 100644 index 0000000..13c27c2 --- /dev/null +++ b/conway.py @@ -0,0 +1,191 @@ +from enum import Enum +from collections import namedtuple +from math import floor +from pygame.sprite import Sprite +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(300,300) + padding: XYPair = XYPair(1, 1) + background_color = Colors.BLACK.value + +class Cell(Sprite): + + 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: + super().__init__() + self.crowding_number = 0 + self.position = position + self.dimensions = dimensions + self.state = state + 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 toggle_state(self): + if self.state == CellStates.ALIVE: + self.set_state(CellStates.DEAD) + elif self.state == CellStates.DEAD: + self.set_state(CellStates.ALIVE) + + 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, render_info: Render_Info): + screen_dimenions = render_info.screen_dimensions + 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 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) + self.matrix[y][x].toggle_state() + + 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(20, 20), render_info) + pygame.display.set_caption("Conway's Game of Life") + + clock = pygame.time.Clock() + + paused = True + running = True + + while running: + + clock.tick(10) + board.draw(screen) + pygame.display.flip() + + if not paused: + board.evolve_state() + + for event in pygame.event.get(): + if event.type == pygame.MOUSEBUTTONDOWN: + board.handle_click(pygame.mouse.get_pos(), render_info) + + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_SPACE: + paused = not paused + + if event.type == pygame.QUIT: + running = False + + +if __name__ == '__main__': + main() \ No newline at end of file