diff --git a/README.md b/README.md index b4dcd6e71..99aa90c43 100644 --- a/README.md +++ b/README.md @@ -122,16 +122,16 @@ Implement at least 3 of the following features: * Create a few sample cell configurations that users can load and run * Add an option that creates a random cell configuration that users can - run -* Add additional cell properties, like color or size, and incorporate + run 1 +* Add additional cell properties, like color or size, and incorporate 2 them into your visualization -* Allow users to specify the speed of the simulation +* Allow users to specify the speed of the simulation 3 * Provide functionality to manually step through the simulation one - generation at a time, as opposed to animating automatically -* Allow users to change the dimension of the grid being displayed + generation at a time, as opposed to animating automatically 4 +* Allow users to change the dimension of the grid being displayed 5 * Given a specific generation, calculate the configuration of cells at that point in time, and jump to that state, bypassing animation (i.e. - skip ahead _n_ generations). + skip ahead _n_ generations). 6 * If you have an idea for a custom feature on this list, run it by your TL or instructor diff --git a/pygameoflife/README.md b/pygameoflife/README.md new file mode 100644 index 000000000..0bb29327f --- /dev/null +++ b/pygameoflife/README.md @@ -0,0 +1,29 @@ + +# Conway's Rules of Life + +Conway's Game of Life is a solitary game where a user interacts with a grid of cells. These cells will survive under specific conditions and die in other specific conditions. These conditions are outlined below: + +1. Any live cell with two or three live neighbours survives. + +2. Any dead cell with three live neighbours becomes a live cell. + +3. All other live cells die in the next generation. Similarly, all other dead cells stay dead. + +## My Version + +The version of the game I created initially randomized the grid cells to one of two binary states (alive or dead). The current generation displays as a caption at the top of the window. The user is able to interact with the game through the use of the keyboard and mouse, outlined in the next section. + +## User Events + +The user can interact with the game by doing the following keypresses on the keyboard: + +Key Press | Action | Explanation +----------|--------|------------ +p | pause | Toggle pause for game state. +c | clear | Clears the screen, setting all cells to dead. +r | randomize | Randomizes the grid, setting all cells to either alive or dead. +h | help | Displays helper functions listed here inside the terminal window. +q | quit | Exits the Game. +n | next | Jump 1 generation forward. +j | jump | Jump X generations forward. Specified with user input in terminal +mouse-click | toggle-value | If the game is paused, user can toggle cells alive and dead by clicking on them. diff --git a/pygameoflife/__init__.py b/pygameoflife/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pygameoflife/__main__.py b/pygameoflife/__main__.py new file mode 100644 index 000000000..9da2711a6 --- /dev/null +++ b/pygameoflife/__main__.py @@ -0,0 +1,6 @@ +# Main function for pygameoflife.py + +# from pygameoflife import GameOfLife + +# game = GameOfLife() +# game.run() \ No newline at end of file diff --git a/pygameoflife/pygameoflife.py b/pygameoflife/pygameoflife.py new file mode 100644 index 000000000..72ad58081 --- /dev/null +++ b/pygameoflife/pygameoflife.py @@ -0,0 +1,314 @@ +import random +import sys +import pygame +import numpy as np + +class GameOfLife: + + def __init__(self, screen_width=1000, screen_height=800, cell_size=30, alive_color=(0, 255, 255), + dead_color=(0, 0, 0), max_fps=10): + """ + Initialize grid, set default game state, initialize screen + :param screen_width: Game window width + :param screen_height: Game window height + :param cell_size: Diameter of circles. + :param alive_color: RGB tuple e.g. (255,255,255) for cells + :param dead_color: RGB tuple e.g. (255,255,255) + :param max_fps: Framerate cap to limit game speed + """ + pygame.init() + # self.max_fps = float(input("Enter your desired FPS between generations (sugested 1-10) ")) + # self.screen_width = int(float(input("Enter your desired Screen Width in pixels. "))) + # self.screen_height = int(float(input("Enter your desired Screen Height in pixels. "))) + # self.cell_size = int(float(input("Enter your desired cell size. "))) + self.max_fps = max_fps + self.screen_width = screen_width + self.screen_height = screen_height + self.cell_size = cell_size + self.alive_color = alive_color + self.dead_color = dead_color + + self.screen = pygame.display.set_mode((self.screen_width, self.screen_height)) + self.clear_screen() + pygame.display.flip() + + # FPS gen-to-gen (allow for user input?) + # self.max_fps = max_fps + + self.active_grid = 0 + self.num_cols = int(self.screen_width / self.cell_size) + self.num_rows = int(self.screen_height / self.cell_size) + self.grids = [] + self.init_grids() + self.set_grid() + + self.paused = False + self.game_over = False + self.generation_counter = 0 + + def init_grids(self): + """ + Create and stores the default active and inactive grid + :return: None + """ + def create_grid(): + """ + Generate an empty 2 grid + :return: + """ + rows = [] + for row_num in range(self.num_rows): + list_of_columns = [0] * self.num_cols + rows.append(list_of_columns) + return rows + self.grids.append(create_grid()) + self.grids.append(create_grid()) + + def set_grid(self, value=None, grid=0): + """ + Set an entire grid at once. Set to a single value or random 0/1. + Examples: + set_grid(0) # all dead + set_grid(1) # all alive + set_grid() # random + set_grid(None) # random + :param grid: Index of grid, for active/inactive (0 or 1) + :param value: Value to set the cell to (0 or 1) + :return: + """ + for r in range(self.num_rows): + for c in range(self.num_cols): + if value is None: + cell_value = random.randint(0, 1) + else: + cell_value = value + self.grids[grid][r][c] = cell_value + + pygame.display.update() + + def display_generation(self): + """ + Displays the current generation at in a caption at the top of the window. + """ + pygame.display.set_caption("Current Generation: %s" %(self.generation_counter)) + + def draw_grid(self): + """ + Given the grid and cell states, draw the cells on the screen + :return: + """ + #self.clear_screen() + for c in range(self.num_cols): + for r in range(self.num_rows): + if self.grids[self.active_grid][r][c] == 1: + color = self.alive_color + else: + color = self.dead_color + pygame.draw.circle(self.screen, + color, + (int(c * self.cell_size + (self.cell_size / 2)), + int(r * self.cell_size + (self.cell_size / 2))), + int(self.cell_size / 2), + 0) + self.display_generation() + pygame.display.flip() + + def clear_screen(self): + """ + Fill whole screen with dead color + :return: + """ + self.screen.fill(self.dead_color) + pygame.display.flip() + + def get_cell(self, row_num, col_num): + """ + Get the alive/dead (0/1) state of a specific cell in active grid + :param row_num: + :param col_num: + :return: 0 or 1 depending on state of cell. Defaults to 0 (dead) + """ + try: + cell_value = self.grids[self.active_grid][row_num][col_num] + except: + cell_value = 0 + return cell_value + + def check_cell_neighbors(self, row_index, col_index): + """ + Get the number of alive neighbor cells, and determine the state of the cell + for the next generation. Determine whether it lives, dies, survives, or is born. + :param row_index: Row number of cell to check + :param col_index: Column number of cell to check + :return: The state the cell should be in next generation (0 or 1) + """ + num_alive_neighbors = 0 + num_alive_neighbors += self.get_cell(row_index - 1, col_index - 1) + num_alive_neighbors += self.get_cell(row_index - 1, col_index) + num_alive_neighbors += self.get_cell(row_index - 1, col_index + 1) + num_alive_neighbors += self.get_cell(row_index, col_index - 1) + num_alive_neighbors += self.get_cell(row_index, col_index + 1) + num_alive_neighbors += self.get_cell(row_index + 1, col_index - 1) + num_alive_neighbors += self.get_cell(row_index + 1, col_index) + num_alive_neighbors += self.get_cell(row_index + 1, col_index + 1) + + # Rules for life and death + if self.grids[self.active_grid][row_index][col_index] == 1: # if alive + if num_alive_neighbors > 3: # Overpopulation, dies + return 0 + if num_alive_neighbors < 2: # Underpopulation, stays dead + return 0 + if num_alive_neighbors == 2 or num_alive_neighbors == 3: # perfect population, come to life + return 1 + elif self.grids[self.active_grid][row_index][col_index] == 0: # if dead + if num_alive_neighbors == 3: + return 1 # come to life + + return self.grids[self.active_grid][row_index][col_index] + + def update_generation(self): + """ + Inspect current generation state, prepare next generation + :return: + """ + self.set_grid(0, self.inactive_grid()) + for r in range(self.num_rows - 1): + for c in range(self.num_cols - 1): + next_gen_state = self.check_cell_neighbors(r, c) + self.grids[self.inactive_grid()][r][c] = next_gen_state + self.active_grid = self.inactive_grid() + + def inactive_grid(self): + """ + Simple helper function to get the index of the inactive grid + If active grid is 0 will return 1 and vice-versa. + :return: + """ + return (self.active_grid + 1) % 2 + + def help(self): + print( + """ + p (pause) - Toggle pause for game state. + c (clear) - Clears the screen, setting all cells to dead. + r (randomize) - Randomizes the grid, setting all cells to either alive or dead. + h (help) - Displays helper functions listed here inside the terminal window. + q (quit) - Exits the Game. + n (next) - Jump 1 generation forward. + j (jump) - Jump X generations forward. Specified with user input in terminal + mouse-click (toggle-value) - If the game is paused, user can toggle cells alive and dead by clicking on them. + """) + + def handle_events(self): + """ + Handle any keypresses + p (pause) - Toggle pause for game state. + c (clear) - Clears the screen, setting all cells to dead. + r (randomize) - Randomizes the grid, setting all cells to either alive or dead. + h (help) - Displays helper functions listed here inside the terminal window. + q (quit) - Exits the Game. + n (next) - Jump 1 generation forward. + j (jump) - Jump X generations forward. Specified with user input in terminal + mouse-click (toggle-value) - If the game is paused, user can toggle cells alive and dead by clicking on them. + """ + for event in pygame.event.get(): + if self.paused: + if event.type == pygame.MOUSEBUTTONDOWN: + if(event.button==1): + mousepos_x, mousepos_y = event.pos + c, r = (int((mousepos_x - self.cell_size / 2) // self.cell_size), + int((mousepos_y - self.cell_size / 2) // self.cell_size)) + #print(event.pos, '->', (r, c)) # Show result. + if r in range(self.num_rows) and c in range(self.num_cols): + # Toggle state of cell: active <--> inactive + if self.grids[self.active_grid][r][c] == 1: + self.grids[self.active_grid][r][c] = 0 + color = self.dead_color + else: + self.grids[self.active_grid][r][c] = 1 + color = self.alive_color + # Redraw cell in its new color. + posn = (int(c * self.cell_size + self.cell_size / 2), + int(r * self.cell_size + self.cell_size / 2)) + #print(' posn:', posn) + pygame.draw.circle(self.screen, color, posn, int(self.cell_size / 2), 0) + + pygame.display.flip() + if event.type == pygame.KEYDOWN: + print("key pressed") + if event.unicode == 'p': # toggle pause + print("Toggling pause.") + if self.paused: + self.paused = False + else: + self.paused = True + elif event.unicode == 'c': # clear screen (set all dead) + print("Clearing screen.") + self.clear_screen() + elif event.unicode == 'r': # randomize grid + print("Randomizing grid.") + self.active_grid = 0 + self.set_grid(None, self.active_grid) # randomize + self.set_grid(0, self.inactive_grid()) # set to 0 + self.draw_grid() + elif event.unicode == 'q': # quit + print("Exiting.") + self.game_over = True + elif event.unicode == 'h': + print('User Events Help Below:') + self.help() + elif event.unicode == 'n': # next generation + print("Stepping forward 1 generation.") + self.update_generation() + self.generation_counter += 1 + self.draw_grid() + elif event.unicode == 'j': # jump forward x generations + skip_step = int(input("Enter # of gens to skip ahead. ")) + # print(f"Jumping forward {skip_step} generations. ") + i = 0 + last_skip = skip_step - 1 + for i in range(0, skip_step): + if i != last_skip: + self.update_generation() + else: + self.update_generation() + self.draw_grid() + # elif event.unicode == 's': + # # change size of the screen in init + # elif event.unicode == 'g': + # # initialize glider grid + # print("Cosper Glider-Gun grid.") + # self.active_grid = self.glider_gun() + # # self.set_grid(None, self.active_grid) # randomize + # # self.set_grid(0, self.inactive_grid()) # set to 0 + # self.draw_grid() + # elif event.unicode == 'i': + # # interact via mouse events (turn cells off and on with : and R-click) + if event.type == pygame.QUIT: + sys.exit() + + def run(self): + """ + Kick off the game and loop forever until quit state + :return: + """ + + clock = pygame.time.Clock() + while True: + if self.game_over: + return + + self.handle_events() + + if not self.paused: + self.update_generation() + self.generation_counter += 1 + self.draw_grid() + + clock.tick(self.max_fps) + +if __name__ == "__main__": + """ + Launch a game of life + """ + GameOfLife().run() \ No newline at end of file diff --git a/venv/README.md b/venv/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/venv/setup.py b/venv/setup.py new file mode 100644 index 000000000..e69de29bb