Skip to content

03 04 #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/challenge1.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/inspectionProfiles/profiles_settings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 44 additions & 0 deletions cell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import pygame
from pygame.locals import Rect

INACTIVE_COLOR = "#16302B"
ACTIVE_COLOR = "#C0E5C8"


class Cell(Rect):
def __init__(self, pos: tuple, dimensions: tuple, active=False):
self.active = active
self.future_state = None

super().__init__(pos, dimensions)

def draw(self, surface):
"""
This method checks what state the cell is in, and draws it in the appropriate color on the provided surface
"""
color = ACTIVE_COLOR if self.active else INACTIVE_COLOR
return pygame.draw.rect(surface, color, self)

def __str__(self):
return "X" if self.active else "_"

def flip(self):
self.active = not self.active

def set_active(self):
self.active = True

def set_inactive(self):
self.active = False

def set_future_state(self, living_neighbors: int):
if self.active and (living_neighbors == 2 or living_neighbors == 3):
self.future_state = True
elif not self.active and living_neighbors == 3:
self.future_state = True
else:
self.future_state = False

def update(self):
self.active = self.future_state
self.future_state = None
60 changes: 60 additions & 0 deletions controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import pygame
import pygame_gui


class EventController:
def __init__(self, start: pygame_gui.elements.UIButton, next: pygame_gui.elements.UIButton, reset: pygame_gui.elements.UIButton):
self.start_button = start
self.next_button = next
self.reset_button = reset

def assess(self, event, state):
if event.type == pygame.QUIT:
return handle_quit(state)

if event.type == pygame.USEREVENT and event.user_type == pygame_gui.UI_BUTTON_PRESSED:
if event.ui_element == self.start_button:
return handle_start_pause(state, event.ui_element)
if event.ui_element == self.reset_button:
return handle_reset(state, self.start_button)
if event.ui_element == self.next_button:
return handle_next(state, self.start_button)
else:
return {}

if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
return handle_grid_click(state)

return {}


def handle_quit(state):
state['is_running'] = False
return state


def handle_reset(state, start_button):
state['grid'].reset()
state['animation_running'] = False
start_button.set_text('Start')

return state


def handle_start_pause(state, button):
state['animation_running'] = not state['animation_running']
button.set_text('Pause') if state['animation_running'] else button.set_text('Start')
return state


def handle_grid_click(state):
pos = pygame.mouse.get_pos()
state['grid'].check_clicks(pos)
return {}


def handle_next(state, start_button):
state['grid'].update()
state['animation_running'] = False
start_button.set_text('Start')
return {}
100 changes: 100 additions & 0 deletions game.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import pygame
import pygame_gui

from grid import Grid
from controller import EventController

GAME_BACKGROUND_COLOR = '#0F0F00'
UI_BACKGROUND_COLOR = '#FFFFFF'

pygame.init()
WINDOW_WIDTH = 1000
WINDOW_HEIGHT = 600
UI_HEIGHT = 100

GAME_HEIGHT = WINDOW_HEIGHT - UI_HEIGHT

BUTTON_WIDTH = 100
BUTTON_HEIGHT = 40

GAME_BACKGROUND_COLOR = '#000000'
UI_BACKGROUND_COLOR = '#000000'

pygame.display.set_caption('Quick Start')
window_surface = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))

game_background = pygame.Surface((WINDOW_WIDTH, GAME_HEIGHT))
game_background.fill(pygame.Color(GAME_BACKGROUND_COLOR))

ui_background = pygame.Surface((WINDOW_WIDTH, UI_HEIGHT))
ui_background.fill(pygame.Color(UI_BACKGROUND_COLOR))

manager = pygame_gui.UIManager((WINDOW_WIDTH, WINDOW_HEIGHT))

manager.preload_fonts([{'name': 'fira_code', 'point_size': 14, 'style': 'bold'}])

start_button = pygame_gui.elements.UIButton(
relative_rect=pygame.Rect((70, GAME_HEIGHT + 30), (BUTTON_WIDTH, BUTTON_HEIGHT)),
text='Start',
tool_tip_text='Start or Pause the simulation',
manager=manager)

reset_button = pygame_gui.elements.UIButton(
relative_rect=pygame.Rect((2*70 + BUTTON_WIDTH, GAME_HEIGHT + 30), (BUTTON_WIDTH, BUTTON_HEIGHT)),
text='Reset',
tool_tip_text='Clear all cells from the screen',
manager=manager)

next_button = pygame_gui.elements.UIButton(
relative_rect=pygame.Rect((3*70 + 2*BUTTON_WIDTH, GAME_HEIGHT + 30), (BUTTON_WIDTH, BUTTON_HEIGHT)),
text='Next',
tool_tip_text='Move the simulation one step forward then pause it',
manager=manager)

rules_button = pygame_gui.elements.UIButton(
relative_rect=pygame.Rect((4*70 + 3*BUTTON_WIDTH, GAME_HEIGHT + 30), (BUTTON_WIDTH, BUTTON_HEIGHT)),
text='Rules',
tool_tip_text='<b>Rule 1:</b> Any active cell with two or three live neighbours survives.<br><b>Rule 2: </b>Any inactive cell with three live neighbours becomes active.<br><b>Rule 3:</b> All other active cells die in the next generation.',
starting_height=7,
manager=manager)

g = Grid((WINDOW_WIDTH, GAME_HEIGHT), window_surface, 15, 15)
g.flip(2, 1)
g.flip(3, 2)
g.flip(1, 3)
g.flip(2, 3)
g.flip(3, 3)

clock = pygame.time.Clock()

game_state = {'is_running': True, 'animation_running': False, 'grid': g}
controller = EventController(start=start_button, next=next_button, reset=reset_button)


def display(state):
window_surface.blit(game_background, (0, 0))
window_surface.blit(ui_background, (0, GAME_HEIGHT))

if state['animation_running']:
state['grid'].update()

state['grid'].draw(window_surface)
manager.draw_ui(window_surface)
pygame.display.update()


while game_state['is_running']:
current_grid = game_state['grid']
time_delta = clock.tick(50) / 1000.0
for event in pygame.event.get():
# pass the event and the game state to the controller
# controller figures out what kind of event to address
# updates the game state accordingly
updated_state = controller.assess(event, game_state)
game_state.update(updated_state)

manager.process_events(event)

manager.update(time_delta)
display(game_state)

102 changes: 102 additions & 0 deletions grid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
from cell import Cell

CELL_OFFSET = 2
PADDING = 7


class Grid:
def __init__(self, screen_dimensions: tuple, surface, width: int, height: int):
self.width = width
self.height = height

screen_width, screen_height = screen_dimensions
effective_width, effective_height = screen_width - 2 * PADDING - (CELL_OFFSET * (width - 1)), screen_height - 2 * PADDING - (CELL_OFFSET * (height - 1))

cell_width, cell_height = (effective_width / width, effective_height / height)
self.cells = [
[Cell((PADDING + x * (cell_width + CELL_OFFSET), PADDING + y * (cell_height + CELL_OFFSET)), (cell_width, cell_height)) for x in range(width)] for y in
range(height)]

def __str__(self):
output = ""
for row in self.cells:
for cell in row:
output += str(cell)
output += "\n"
return output

def flip(self, col: int, row: int):
if col < 0 or col >= self.width:
raise RuntimeError(
f"error updating cell at column {col}: expected column number between 0 and {self.width - 1}")
if row < 0 or row >= self.height:
raise RuntimeError(
f"error updating cell at row {row}: expected column number between 0 and {self.height - 1}")

self.cells[row][col].flip()

def __compute_future_states(self):
# navigate through the grid, for each cell find its valid neighbors
for row_index, row in enumerate(self.cells):
for col_index, cell in enumerate(row):
cell.set_future_state(self.__count_living_neighbors(col_index, row_index))

def update(self):
self.__compute_future_states()
for row in self.cells:
for cell in row:
cell.update()

def draw(self, surface):
for row in self.cells:
for cell in row:
cell.draw(surface)

def check_clicks(self, pos):
for row in self.cells:
for cell in row:
if cell.collidepoint(pos):
cell.flip()

def reset(self):
for row in self.cells:
for cell in row:
cell.set_inactive()

# cells have up to 8 neighbours, except cells in the boundary rows and columns.
# this iterator yields a given position's neighbors
def __count_living_neighbors(self, col: int, row: int):
if col < 0 or col >= self.width:
raise RuntimeError(
f"error updating cell at column {col}: expected column number between 0 and {self.width - 1}")
if row < 0 or row >= self.height:
raise RuntimeError(
f"error updating cell at row {row}: expected column number between 0 and {self.height - 1}")

count = 0
# top left
if row > 0 and col > 0 and self.cells[row - 1][col - 1].active:
count += 1
# top
if row > 0 and self.cells[row - 1][col].active:
count += 1
# top right
if row > 0 and col < self.width - 1 and self.cells[row - 1][col + 1].active:
count += 1
# right
if col < self.width - 1 and self.cells[row][col + 1].active:
count += 1
# bottom right
if row < self.height - 1 and col < self.width - 1 and self.cells[row + 1][col + 1].active:
count += 1
# bottom
if row < self.height - 1 and col < self.width - 1 and self.cells[row + 1][col].active:
count += 1
# bottom left
if row < self.height - 1 and col < self.width - 1 and self.cells[row + 1][col - 1].active:
count += 1
# left
if col < self.width - 1 and self.cells[row][col - 1].active:
count += 1

return count
Binary file added requirements.txt
Binary file not shown.