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