initial commit
This commit is contained in:
191
conway.py
Normal file
191
conway.py
Normal file
@@ -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()
|
||||
Reference in New Issue
Block a user