class Game: """ Game class controls the flow of the game. This class receives inputs from users and pass the inputs to other classes to update the state of a field. === Private Attributes === _dim: the dimensions of a game. _game_over: declares if the game ended to display score view _field: an instance of a Field class representing a game field. _in_game: declares if the window displays the game view _view_leaderboard: declares if the window displays the leaderboard view. _menu_index: keeps the record of selected_menu. _screen: the pygame screen onto which the different scenes will display. _view_title: declares if the window displays the main menu view. === Public Attributes === player: the player's alias. leaderboard: a LeaderBoard class instance to store the player's score. hand: declares handedness of player. 0 for left-handed; 1 for right-handed """ # Attributes _dim: Tuple[int, int] _in_game: bool _game_over: bool _view_leaderboard: bool _view_title_screen: bool _menu_index: int _field: Optional[Field] _screen: pygame.Surface player: str _leaderboard: Optional[LeaderBoard] hand: int # Constants TEXT_COLOR = (50, 50, 50) TITLE_SCREEN_COLOR = (255, 255, 255) def __init__(self) -> None: """Initializer of game class with player <player_name>. """ self._dim = (800, 800) self._in_game = False self._game_over = False self._menu_index = 0 self._field = None self.player = None self._view_leaderboard = False self._view_title = True self.hand = None def main(self) -> None: """Run the game. The loop continues until either the snake hits the wall or eats his tail. """ # Initialize pygame, generate screen and field self._set_player_name() self._leaderboard = LeaderBoard(self.player) self._screen = self._init_screen() pygame.init() clock = pygame.time.Clock() fps = 5 while True: self._handle_screen(self._screen) pygame.display.update() clock.tick(fps) # -------------------------------------------------------- # Helper function of main # -------------------------------------------------------- def _set_player_name(self) -> None: """Ask a user to choose their name and controls they wish to use. """ self.player = input("Enter your name \n") invalid_input = True while invalid_input: hand_input = input("If you want to play a game with WASD, press 0, or press 1 to use arrow keys \n") try: self.hand = int(hand_input) if self.hand not in [0, 1]: print("Invalid input, Please enter 0 or 1.") else: invalid_input = False except ValueError: print("Invalid input, Please enter 0 or 1.") def _init_screen(self) -> pygame.Surface: """Initialize title screen""" screen = pygame.display.set_mode(self._dim, pygame.RESIZABLE) screen.fill(self.TITLE_SCREEN_COLOR) pygame.display.set_caption("Snake game") self._menu_index = 0 return screen def _handle_screen(self, screen: pygame.Surface): """Choose which screen to show""" # if in_leader_board and not in_title and not in_game: if self._view_title: self._display_title_screen() elif self._in_game: self._run_game(screen) elif self._game_over: self._display_score() else: self._display_scoreboard() # -------------------------------------------------------- # Main function to create view # -------------------------------------------------------- def _display_minimenu(self, title: str, size: int, where: int = None, show_score: bool = False, show_leader_board: bool = False) -> None: """Displays a screen with <title> and font <size>. Method also adds main menu options relative to <where> """ # Enable exiting game and process user inputs for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() elif event.type == pygame.KEYDOWN: self._handle_key_input(event.key) elif event.type == pygame.VIDEORESIZE: self._dim = event.dict['size'] self._screen = pygame.display.set_mode(self._dim, pygame.RESIZABLE) pygame.display.flip() else: pass # self._screen = pygame.display.set_mode(self._dim, RESIZABLE) self._screen.fill(self.TITLE_SCREEN_COLOR) self._create_title(title, size) self._create_menu_options(where) if show_score: margin_x = self._calc_margin_x(150) self._create_menu_label("Game Over!", (margin_x, 200)) if self._field is None: results = "" else: results = self.player + ": " + str(self._field.get_score()) result_margin = self._calc_margin_x(len(results) * 13) self._create_menu_label(results, (result_margin, 250)) elif show_leader_board: self._format_scoreboard() # -------------------------------------------------------- # Helper function of _display_mini_menu # -------------------------------------------------------- def _create_menu_label(self, label_text: str, location: Tuple[int, int]) -> None: """Draw menu label from given label text at given location""" menu_font = pygame.font.SysFont("Arial", 36) menu_label = menu_font.render(label_text, 1, self.TEXT_COLOR) self._screen.blit(menu_label, location) def _draw_menu_selection_arrow(self, x_location: int, y_location: int) -> None: """Draw menu selection arrow into screen""" menu_arrow_location = [[x_location, y_location + 2], [x_location, y_location + 22], [x_location + 20, y_location + 12]] pygame.draw.polygon(self._screen, self.TEXT_COLOR, menu_arrow_location) def _create_title(self, title: str, size: int,): """Create title label on the screen""" title_font = pygame.font.SysFont("monospace", size) title_label = title_font.render(title, 1, self.TEXT_COLOR) title_width, title_height = title_label.get_size() new_location = ((self._dim[0] - title_width) // 2, 50) self._screen.blit(title_label, new_location) def _create_menu_options(self, where: Optional[int]) -> None: """Create menu options with an arrow showing selected option""" margin_x = self._calc_margin_x(190) + 35 if where is not None: self._create_menu_label("Play", (margin_x, where)) self._create_menu_label("Leaderboard", (margin_x, where + 40) ) self._create_menu_label("Quit", (margin_x, where + 80)) self._draw_menu_selection_arrow(margin_x - 35, [where, where + 40, where + 80][self._menu_index]) else: self._create_menu_label("Play", (margin_x, 300)) self._create_menu_label("Leaderboard", (margin_x, 340), ) self._create_menu_label("Quit", (margin_x, 380)) self._draw_menu_selection_arrow(margin_x - 35, [300, 340, 380][self._menu_index]) def _calc_margin_x(self, width: int) -> int: """Calculate horizontal margins based on given width""" margin = (self._screen.get_width() - width) // 2 if margin < 0: margin *= -1 return margin def _format_scoreboard(self) -> None: """Formats game history to display top 10 scores and players""" winners = self._leaderboard.get_top_ten() i, j = 1, 1 for entry in winners: player, score = entry margin_x = self._calc_margin_x(700) self._create_menu_label(player, (margin_x, 100 + j * 40)) self._create_menu_label(score, (self._screen.get_width() - margin_x - 50, 100 + j * 40)) i += 1 j += 1 # -------------------------------------------------------- # Title Screen # -------------------------------------------------------- def _display_title_screen(self) -> None: """Draw menu option arrow and update view""" # Clear current view contents self._display_minimenu("Snake game", 80) # -------------------------------------------------------- # key_input_handler for title/score # -------------------------------------------------------- def _handle_key_input(self, key_value: int) -> None: """Handle key inputs in title screen""" if key_value == pygame.K_UP: self._menu_index -= 1 if self._menu_index < 0: self._menu_index = 2 elif key_value == pygame.K_DOWN: self._menu_index += 1 if self._menu_index > 2: self._menu_index = 0 elif key_value == pygame.K_RETURN: self._change_view_flags() def _change_view_flags(self) -> None: """Helper function of handle_key_input""" if self._menu_index == 0: # user chooses to play self._in_game = True self._view_leaderboard = False self._view_title = False self._game_over = False self._field = None elif self._menu_index == 1: # user chooses to view leaderboard self._in_game = False self._view_leaderboard = True self._view_title = False self._game_over = False else: # user chooses to exit GUI pygame.quit() sys.exit() # -------------------------------------------------------- # run game # -------------------------------------------------------- def _run_game(self, screen: pygame.Surface) -> None: """Recursively call this method until the game is over. This method should receive user inputs and update state of a field """ key = None for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() elif event.type == pygame.KEYDOWN: key = event.key if self._field is None: self._field = Field(screen, self.hand) is_game_over = self._field.display_field(key) if is_game_over: self._game_over = True self._in_game = False score = self._field.get_score() self._leaderboard.set_player_score(score) self._display_score() def _display_score(self) -> None: """Display game instance results""" self._display_minimenu("Snake game", 80, None, True) # -------------------------------------------------------- # Leader Board # -------------------------------------------------------- def _display_scoreboard(self) -> None: """Display leaderboard screen with top 10 scores""" history_size = self._leaderboard.get_history_size() self._display_minimenu("LEADERBOARD", 54, history_size * 55, False, True)
class Game: """ Game class controls the flow of the game. This class receives inputs from users and pass the inputs to other classes to update the state of a field. === Attributes === _field: an instance of a field class representing a game field. _in_game: declares if the window displays the game view _menu_index: keeps the record of selected_menu """ # Attributes _dim: Tuple[int, int] _in_game: bool _menu_index: int _field: Optional[Field] # Constants COLOR_WHITE = (255, 255, 255) TITLE_SCREEN_COLOR = (0, 0, 80) def __init__(self) -> None: """Initializes Game class TODO: write a doctest """ self._dim = (300, 300) self._in_game = False self._menu_index = 0 self._field = None def main(self) -> None: """Run the game. The loop continues until either the snake hits the wall or eats his tail TODO: write a doctest """ # Initialize pygame, generate screen and field pygame.init() screen = self._init_screen() clock = pygame.time.Clock() fps = 30 while True: if not self._in_game: self._display_title_screen(screen) else: self._run_game(screen) clock.tick(fps) def _init_screen(self) -> pygame.Surface: """Initialize title screen.""" screen = pygame.display.set_mode((600, 600)) screen.fill(self.TITLE_SCREEN_COLOR) pygame.display.set_caption("Snake game") self._menu_index = 0 self._field = Field(screen) return screen def _clear_title_screen(self, screen: pygame.Surface) -> None: """Clear title screen and draw title and menu options.""" screen.fill(self.TITLE_SCREEN_COLOR) title_font = pygame.font.SysFont("monospace", 80) title_label = title_font.render("Snake game", 1, (255, 255, 255)) screen.blit(title_label, (130, 100)) # create title select self._create_menu_label(screen, "Play", (270, 300)) self._create_menu_label(screen, "Score", (270, 360)) self._create_menu_label(screen, "Quit", (270, 420)) def _display_title_screen(self, screen: pygame.Surface) -> None: """Draw menu option arrow and update view.""" # Clear current view contents self._clear_title_screen(screen) # Receive user inputs for event in pygame.event.get(): if event.type == QUIT: pygame.quit() sys.exit() elif event.type == KEYDOWN: self._handle_key_input(event.key) # Draw menu selection arrow self._draw_menu_selection_arrow(screen, [300, 360, 420][self._menu_index]) pygame.display.update() def _handle_key_input(self, key_value: int) -> None: """Handle key inputs in title screen""" if key_value == pygame.K_UP: self._menu_index -= 1 if self._menu_index < 0: self._menu_index = 2 elif key_value == pygame.K_DOWN: self._menu_index += 1 if self._menu_index > 2: self._menu_index = 0 elif key_value == pygame.K_RETURN: if self._menu_index == 0: self._in_game = True elif self._menu_index == 0: # TODO add score view pass else: pygame.quit() sys.exit() def _create_menu_label(self, screen: pygame.Surface, label_text: str, location: Tuple[int, int]) -> None: """Draw menu label from given label text at given location.""" menu_font = pygame.font.SysFont("monospace", 40) menu_label = menu_font.render(label_text, 1, self.COLOR_WHITE) screen.blit(menu_label, location) def _draw_menu_selection_arrow(self, screen: pygame.Surface, y_location: int) -> None: """Draw menu selection arrow into screen""" menu_arrow_location = [[200, y_location + 2], [200, y_location + 22], [220, y_location + 12]] pygame.draw.polygon(screen, self.COLOR_WHITE, menu_arrow_location) def _is_game_over(self) -> bool: """Check the state of a field and return true if the game over condition is met. TODO: and write a doctest """ boundaries = [(0, 0), (0, self._dim[0]), (0, self._dim[1]), self._dim] if self._field.get_snake_head() in boundaries or \ self._field.get_snake_head() == self._field.get_snake_tail(): return True return False def _run_game(self, screen: pygame.Surface) -> None: """ recursively call this method until the game is over. This method should receive user inputs and update state of a field TODO: add a method body and write a doctest """ self._field.display_field() for event in pygame.event.get(): if event.type == QUIT: pygame.quit() sys.exit() elif event.type == KEYDOWN: # TODO pass key event to snake or field to draw self._field.display_field() pygame.display.update()