diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/challenge1.iml b/.idea/challenge1.iml
new file mode 100644
index 0000000..d0876a7
--- /dev/null
+++ b/.idea/challenge1.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..dc9ea49
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..02c1e86
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/cell.py b/cell.py
new file mode 100644
index 0000000..7c9c344
--- /dev/null
+++ b/cell.py
@@ -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
diff --git a/controller.py b/controller.py
new file mode 100644
index 0000000..f9aca81
--- /dev/null
+++ b/controller.py
@@ -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 {}
diff --git a/game.py b/game.py
new file mode 100644
index 0000000..2ab85ab
--- /dev/null
+++ b/game.py
@@ -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='Rule 1: Any active cell with two or three live neighbours survives.
Rule 2: Any inactive cell with three live neighbours becomes active.
Rule 3: 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)
+
diff --git a/grid.py b/grid.py
new file mode 100644
index 0000000..070bb4e
--- /dev/null
+++ b/grid.py
@@ -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
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..7dc88c0
Binary files /dev/null and b/requirements.txt differ