import contextlib from functools import partial import random import sys from typing import Callable, NoReturn, Optional import pygame from .screen import Screen from ..piece import * from ..playfield import PlayField from ..controller import Input, Controller, KeyboardController, JoystickController from ..misc import Pause class WouldCollide(Exception): pass class PlayerQuit(Exception): pass right_pane_canvas = pygame.Surface((300, 1000)) right_pane_canvas.fill((255, 255, 255)) class Player: def __init__(self, controller: Controller, starting_level: int): self.controller = controller self.playfield = PlayField() self.current_piece, self.current_piece_position = self.generate_piece() self.next_piece, self.next_piece_position = self.generate_piece() self.level = self.starting_level = starting_level self.score = 0 self.lines_burnt = 0 self.das = 0 self.pressing_down_countdown: Optional[int] = None self.piece_drop_frames = 0 self.grid_canvas = pygame.Surface((500, 1000)) self.piece_preview_canvas = pygame.Surface((200, 200)) self.score_canvas = pygame.Surface((296, 50)) self.level_canvas = pygame.Surface((296, 50)) def generate_piece(self) -> tuple[Piece, list[int]]: # We may want to make this a function outside the class piece = random.choice((TPiece, SPiece, IPiece, ZPiece, SquarePiece, LPiece, JPiece))() for row_id, row in enumerate(piece.elements): if list(filter(lambda x: x is not None, row)): break initial_y_position = -row_id initial_x_position = (len(self.playfield.grid[0]) // 2) - (len(piece.elements[0]) // 2) return (piece, [initial_y_position, initial_x_position]) def lock_piece(self) -> None: if self.has_collision(self.current_piece_position[0], self.current_piece_position[1]): raise WouldCollide() for row_id, row in enumerate(self.current_piece.elements): for col_id, element in enumerate(row): if element is None: continue self.playfield.grid[row_id + self.current_piece_position[0]][col_id + self.current_piece_position[1]] = element count = self.playfield.burn_rows() if count == 1: print("Single") rate = 1. elif count == 2: print("Double") rate = 2.5 elif count == 3: print("Triple") rate = 7.5 elif count == 4: print("Tetris!") rate = 30. else: rate = 0. self.lines_burnt += count self.score += int(self.level * 40 * rate) if self.lines_burnt >= self.level * 10: self.level += 1 self.current_piece, self.current_piece_position = self.next_piece, self.next_piece_position self.next_piece, self.next_piece_position = self.generate_piece() self.refresh_piece_preview_canvas() def has_collision(self, y: int, x: int) -> bool: try: for row_id, row in enumerate(self.current_piece.elements): for col_id, element in enumerate(row): if element is None: continue if row_id + y < 0: continue if col_id + x < 0: return True if self.playfield.grid[row_id + y][col_id + x] is not None: return True except IndexError: return True return False def move_piece_down(self) -> None: if not self.has_collision(self.current_piece_position[0] + 1, self.current_piece_position[1]): self.current_piece_position[0] += 1 else: raise WouldCollide() def move_piece_up(self) -> None: if not self.has_collision(self.current_piece_position[0] - 1, self.current_piece_position[1]): self.current_piece_position[0] -= 1 else: raise WouldCollide() def move_piece_left(self) -> None: if not self.has_collision(self.current_piece_position[0], self.current_piece_position[1] - 1): self.current_piece_position[1] -= 1 else: raise WouldCollide() def move_piece_right(self) -> None: if not self.has_collision(self.current_piece_position[0], self.current_piece_position[1] + 1): self.current_piece_position[1] += 1 else: raise WouldCollide() def rotate_piece_counter_clockwise(self) -> None: self.current_piece.rotate_counter_clockwise() if self.has_collision(self.current_piece_position[0], self.current_piece_position[1]): self.current_piece.rotate_clockwise() raise WouldCollide() def rotate_piece_clockwise(self) -> None: self.current_piece.rotate_clockwise() if self.has_collision(self.current_piece_position[0], self.current_piece_position[1]): self.current_piece.rotate_counter_clockwise() raise WouldCollide() def handle_input_pressed(self, event: pygame.event.Event) -> None: if self.controller.get_input_down(event) == Input.QUIT: raise PlayerQuit() if self.controller.get_input_down(event) == Input.PAUSE: raise Pause() with contextlib.suppress(WouldCollide): if self.controller.get_input_down(event) == Input.MOVE_RIGHT: self.move_piece_right() self.das = 0 if self.controller.get_input_down(event) == Input.MOVE_LEFT: self.move_piece_left() self.das = 0 if self.controller.get_input_down(event) == Input.ROTATE_CLOCKWISE: self.rotate_piece_clockwise() if self.controller.get_input_down(event) == Input.ROTATE_COUNTER_CLOCKWISE: self.rotate_piece_counter_clockwise() if self.controller.get_input_down(event) == Input.MOVE_DOWN: self.piece_drop_frames = 0 self.pressing_down_countdown = 3 try: self.move_piece_down() except WouldCollide: self.lock_piece() def handle_input_released(self, event: pygame.event.Event) -> None: if self.controller.get_input_up(event) == Input.MOVE_DOWN: self.pressing_down_countdown = None def refresh_piece_preview_canvas(self) -> None: self.piece_preview_canvas.fill(black) non_empty_rows = list() for row in self.next_piece.elements: if any(map(lambda element: element is not None, row)): non_empty_rows.append(row) non_empty_cols = set() for row in self.next_piece.elements: for col_id, element in enumerate(row): if element is not None: non_empty_cols.add(col_id) y_offset = (4 - len(non_empty_rows)) / 2 x_offset = (4 - len(non_empty_cols)) / 2 # Display the next piece for row_idx, row in enumerate(non_empty_rows): for col_idx, element in enumerate(row): if element is not None: self.piece_preview_canvas.blit(element, ((col_idx + x_offset) * 50 + 1, (row_idx + y_offset) * 50 + 1)) def refresh_grid_canvas(self) -> None: self.grid_canvas.fill(black) for row_idx, row in enumerate(self.playfield.grid): for col_idx, element in enumerate(row): if element is not None: self.grid_canvas.blit(element, (col_idx * 50 + 1, row_idx * 50 + 1)) # Display the current piece for row_idx, row in enumerate(self.current_piece.elements): for col_idx, element in enumerate(row): if element is not None: self.grid_canvas.blit(element, ((col_idx + self.current_piece_position[1]) * 50 + 1, (row_idx + self.current_piece_position[0]) * 50 + 1)) def handle_input_pressed(instance, players: list[Player], event: pygame.event.Event) -> None: for player in players: if isinstance(player.controller, instance): player.handle_input_pressed(event) def handle_input_released(instance, players: list[Player], event: pygame.event.Event) -> None: for player in players: if isinstance(player.controller, instance): player.handle_input_released(event) # Number of frames frames_per_gridcell = [48, 43, 38, 33, 28, 23, 18, 13, 8, 6, 5, 5, 5, 4, 4, 4, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1] class InGame(Screen): def __init__(self, players: list[Player], screen: pygame.surface.Surface): self.players: list[Player] = players self.screen: pygame.surface.Surface = screen self.event_handler: dict[int, Callable[[pygame.event.Event], None]] = {} def exit(_) -> NoReturn: sys.exit() self.event_handler[pygame.QUIT] = exit self.event_handler[pygame.KEYDOWN] = partial(handle_input_pressed, KeyboardController, self.players) self.event_handler[pygame.KEYUP] = partial(handle_input_released, KeyboardController, self.players) self.event_handler[pygame.JOYBUTTONDOWN] = partial(handle_input_pressed, JoystickController, self.players) self.event_handler[pygame.JOYBUTTONUP] = partial(handle_input_released, JoystickController, self.players) def refresh_right_pane_canvas(self) -> None: for player in self.players: player.level_canvas.fill(black) player.score_canvas.fill(black) if pygame.font: score_font = pygame.font.Font(None, 56) player.score_canvas.blit(score_font.render(f"{player.score:08d}", True, white), (0, 0)) player.level_canvas.blit(score_font.render(f"{player.level:08d}", True, white), (0, 0)) right_pane_canvas.blit(player.score_canvas, (2, 10)) right_pane_canvas.blit(player.level_canvas, (2, 70)) right_pane_canvas.blit(player.piece_preview_canvas, (50, 200)) def refresh(self) -> None: for player in self.players: player.refresh_grid_canvas() self.screen.blit(player.grid_canvas, (0, 0)) self.refresh_right_pane_canvas() self.screen.blit(right_pane_canvas, (501, 0)) def oneframe(self) -> None: for player in self.players: player.piece_drop_frames += 1 try: for event in pygame.event.get(): with contextlib.suppress(KeyError): self.event_handler[event.type](event) except Pause: pygame.event.clear() raise for player in self.players: player.das += 1 if player.das == 16: with contextlib.suppress(WouldCollide): if player.controller.is_pressed(Input.MOVE_RIGHT): player.move_piece_right() if player.controller.is_pressed(Input.MOVE_LEFT): player.move_piece_left() player.das = 10 if player.pressing_down_countdown == 0: try: player.move_piece_down() except WouldCollide: player.lock_piece() player.pressing_down_countdown = 2 elif player.pressing_down_countdown is not None: player.pressing_down_countdown -= 1 if player.piece_drop_frames >= frames_per_gridcell[player.level - 1]: player.piece_drop_frames = 0 try: player.move_piece_down() except WouldCollide: player.lock_piece()