import pygame import pygame.draw as draw from collections import namedtuple, defaultdict from typing import NamedTuple, Any, Iterable, Optional, Callable from functools import cache import random import math from enum import Enum, auto Cell = namedtuple("Cell", ["x", "y"]) Size = namedtuple("Size", ["width", "height"]) ColorSet = namedtuple("ColorSet", ["norm", "light", "dark"]) class Direction(int, Enum): LEFT = -1 RIGHT = 1 def make_matrix(w: int, h: int, elt: Any = None) -> list[list]: if not w: return [] return [[elt] * w for _ in range(h)] def flip(mat: list[list]) -> list[list]: return list(reversed(mat)) def transpose(mat: list[list]) -> list[list]: if not mat: return [] out = make_matrix(len(mat), len(mat[0])) for x in range(len(mat[0])): for y in range(len(mat)): out[x][y] = mat[y][x] return out def mirror(mat: list[list]) -> list[list]: return [list(reversed(row)) for row in mat] class Tetromino(NamedTuple): color: str shape: list[list] @property def width(self): return len(self.shape[0]) @property def height(self): return len(self.shape) def rotated(self, times: int): """Return self rotated TIMES times to the right.""" times %= 4 if times == 0: return self if times == 1: return Tetromino(self.color, mirror(transpose(self.shape))) if times == 2: return Tetromino(self.color, mirror(flip(self.shape))) if times == 3: return Tetromino(self.color, flip(transpose(self.shape))) def __repr__(self): out = [] for row in self.shape: for cell in row: out.append("*" if cell else " ") out.append("\n") return "".join(out) def __eq__(self, other): return id(self) == id(other) def __hash__(self): return id(self) class HashableDict(dict): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def __eq__(self, other): return id(self) == id(other) def __hash__(self): return id(self) class Action(NamedTuple): class Type(Enum): # In Game ROTATE_LEFT = auto() ROTATE_RIGHT = auto() DROP = auto() SWAP_HOLD = auto() MOVE_LEFT = auto() MOVE_RIGHT = auto() OPEN_HELP = auto() # Game over (also in game) RESTART = auto() # Help CLOSE_HELP = auto() key: int type: Type repeat: bool desc: str @staticmethod def make_map(*acts): return HashableDict({act.key: act for act in acts}) class Font: class Glyph(NamedTuple): char: str bitmap: list[str] @property def width(self): return len(self.bitmap[0]) @property def height(self): return len(self.bitmap) @property def size(self) -> Size: return Size(self.width, self.height) def __init__( self, scale: int, kern: int, row_kern: int, glyphs: Iterable[Glyph] | dict[str, Glyph], ): self.scale = scale self.kern = kern self.row_kern = row_kern if isinstance(glyphs, dict): self.glyphs: dict[str, Font.Glyph] = dict(glyphs) else: self.glyphs: dict[str, Font.Glyph] = {g.char: g for g in glyphs} def scaled( self, new_scale: int, new_kern: Optional[int], new_row_kern: Optional[int], ): return Font( new_scale, self.kern if new_kern is None else new_kern, self.row_kern if new_row_kern is None else new_row_kern, self.glyphs, ) @cache def compute_extents(self, text: str) -> Size: text = text.lower() total_width = 0 total_height = 0 last_width = 0 last_height = 0 for char in text: if char == "\n": total_width = max(total_width, last_width) if last_width == 0: # empty line last_height = self.glyphs[" "].height * self.scale last_width = 0 total_height += last_height + self.row_kern last_height = 0 else: glyph = self.glyphs[char] last_width += glyph.width * self.scale + self.kern last_height = max(last_height, glyph.height * self.scale) total_width = max(last_width, total_width) - self.kern if total_width == 0: # empty line last_height = self.glyphs[" "].height * self.scale total_height += last_height return Size(total_width, total_height) def render_text( self, text: str, dest: pygame.surface.Surface, x: int, y: int, color ): text = text.lower() initial_x = x max_height = 0 for char in text: if char == "\n": if x == initial_x: # empty row max_height = self.glyphs[" "].height * self.scale y += max_height + self.row_kern max_height = 0 x = initial_x else: glyph = self.glyphs[char] dest.blit(self.render_glyph(char, color), (x, y)) x += glyph.width * self.scale + self.kern max_height = max(max_height, glyph.height * self.scale) def center_text( self, text: str, dest: pygame.surface.Surface, x: int, y: int, color ): text = text.lower() exts = self.compute_extents(text) self.render_text( text, dest, x - (exts.width // 2), y - (exts.height // 2), color ) @cache def render_glyph(self, char: str, color) -> pygame.surface.Surface: char = char.lower() glyph = self.glyphs[char] size = tuple(map(lambda d: d * self.scale, glyph.size)) surf = pygame.surface.Surface(size, pygame.SRCALPHA) for y, row in enumerate(glyph.bitmap): for x, cell in enumerate(row): if cell != " ": pygame.draw.rect( surf, color, ( x * self.scale, y * self.scale, self.scale, self.scale, ), ) return surf class LazyVariable: def __init__(self, thunk: Callable[[], Any]): self.thunk = thunk self.need_eval = True self.value = None def __get__(self, instance: Any, owner: Optional[type] = None): if self.need_eval: self.value = self.thunk() self.need_eval = False return self.value def __set__(self, instance: Any, value: Any): self.value = value self.need_eval = False class Game: PLACE_POINTS = 10 CLEAR_POINTS = 100 MOVE_SPEED = 20 # cells per second NORMAL_DROP_SPEED = 3 # cells per second FAST_DROP_SPEED = 30 # cells per second CLEAR_SPEED = 5 # frames per block BOARD_SIZE = Size(10, 20) # Cell count CELL_SIZE = Size(30, 30) CELL_BORDER_SIZE = CELL_SIZE.width // 10 CONTROLS_WIDTH = 160 FRAMERATE = 120 WINDOW_SIZE = Size( (BOARD_SIZE.width + 2) * CELL_SIZE.width + CONTROLS_WIDTH, (BOARD_SIZE.height + 2) * CELL_SIZE.height, ) PALETTE = { "gray": ColorSet("#787878", "#9a9a9a", "#303030"), "purple": ColorSet("#9a00cd", "#cd00ff", "#66009a"), "yellow": ColorSet("#cdcd00", "#ffff00", "#9a9a00"), "red": ColorSet("#cd0000", "#ff0000", "#9a0000"), "orange": ColorSet("#cd6600", "#ff8900", "#9a4200"), "blue": ColorSet("#0000cd", "#0000ff", "#00009a"), "aqua": ColorSet("#00cdcd", "#00ffff", "#009a9a"), "green": ColorSet("#00cd00", "#00ff00", "#009a00"), } TETROMINOS = [ Tetromino("aqua", [[1, 1, 1, 1]]), Tetromino("yellow", [[1, 1], [1, 1]]), Tetromino("purple", [[1, 1, 1], [0, 1, 0]]), Tetromino("blue", [[0, 1], [0, 1], [1, 1]]), Tetromino("orange", [[1, 0], [1, 0], [1, 1]]), Tetromino("green", [[0, 1, 1], [1, 1, 0]]), Tetromino("red", [[1, 1, 0], [0, 1, 1]]), ] GAME_ACTION_MAP = Action.make_map( Action( pygame.K_z, Action.Type.ROTATE_LEFT, False, "Rotate the current piece left.", ), Action( pygame.K_x, Action.Type.ROTATE_RIGHT, False, "Rotate the current piece right.", ), Action( pygame.K_s, Action.Type.SWAP_HOLD, False, "Swap the current and held pieces.", ), Action( pygame.K_DOWN, Action.Type.DROP, True, "Drop the current piece." ), Action( pygame.K_LEFT, Action.Type.MOVE_LEFT, True, "Move the current piece left.", ), Action( pygame.K_RIGHT, Action.Type.MOVE_RIGHT, True, "Move the current piece right.", ), Action( pygame.K_r, Action.Type.RESTART, False, "Start a new game.", ), Action( pygame.K_h, Action.Type.OPEN_HELP, False, "Toggle the help menu.", ), ) GAME_OVER_ACTION_MAP = Action.make_map( Action(pygame.K_r, Action.Type.RESTART, False, "Start a new game.") ) HELP_ACTION_MAP = Action.make_map( Action( pygame.K_ESCAPE, Action.Type.CLOSE_HELP, False, "Close the help menu.", ), Action( pygame.K_h, Action.Type.CLOSE_HELP, False, "Close the help menu.", ), ) # The actual font data is long, so I put it at the end FONT = LazyVariable(lambda: Font(6, 3, 3, GLYPH_DATA)) SMALL_FONT = LazyVariable(lambda: Game.FONT.scaled(4, 2, 2)) TINY_FONT = LazyVariable(lambda: Game.FONT.scaled(2, 2, 2)) CONTROL_DISPLAY_BORDER = 5 INFO_BOX_BORDER_SIZE = 5 HELP_MENU_INSET = 10 CONTROLS_START_X = (BOARD_SIZE.width + 2) * CELL_SIZE.width @staticmethod @cache def make_help_string(am: dict[int, Action]): key_names = [pygame.key.name(k) for k in am.keys()] max_key_len = max(map(len, key_names)) out = "" for act in am.values(): if out: out += "\n" out += f"{pygame.key.name(act.key).rjust(max_key_len)}: {act.desc}" return out def __init__(self): self._game_over = False self.high_score = 0 self._score = 0 @property def game_over(self) -> bool: return self._game_over @game_over.setter def game_over(self, newval: bool): if newval != self._game_over: self.pending_actions = [] self._game_over = newval if self._game_over: self.action_map = Game.GAME_OVER_ACTION_MAP else: self.action_map = Game.GAME_ACTION_MAP @property def score(self) -> int: return self._score @score.setter def score(self, newval: int): self._score = newval if newval > self.high_score: self.high_score = newval def random_tetromino(self): if not self.cur_random_peice_seq: self.cur_random_peice_seq = list(Game.TETROMINOS) random.shuffle(self.cur_random_peice_seq) return self.cur_random_peice_seq.pop(0) def draw_block(self, x: int, y: int, color, target=None): """Draw a block at (X, Y). Coordinates are for the top left corner.""" if target is None: target = self.screen draw.rect( target, Game.PALETTE[color].norm, (x, y, Game.CELL_SIZE.width, Game.CELL_SIZE.height), ) draw.polygon( target, Game.PALETTE[color].light, [ (x, y), (x + Game.CELL_SIZE.width, y), ( x + Game.CELL_SIZE.width - Game.CELL_BORDER_SIZE, y + Game.CELL_BORDER_SIZE, ), (x + Game.CELL_BORDER_SIZE, y + Game.CELL_BORDER_SIZE), ( x + Game.CELL_BORDER_SIZE, y + Game.CELL_SIZE.width - Game.CELL_BORDER_SIZE, ), (x, y + Game.CELL_SIZE.height), (x, y), ], ) draw.polygon( target, Game.PALETTE[color].dark, [ (x + Game.CELL_SIZE.width, y), ( x + Game.CELL_SIZE.width - Game.CELL_BORDER_SIZE, y + Game.CELL_BORDER_SIZE, ), ( x + Game.CELL_SIZE.width - Game.CELL_BORDER_SIZE, y + Game.CELL_SIZE.height - Game.CELL_BORDER_SIZE, ), ( x + Game.CELL_BORDER_SIZE, y + Game.CELL_SIZE.width - Game.CELL_BORDER_SIZE, ), (x, y + Game.CELL_SIZE.height), (x + Game.CELL_SIZE.width, y + Game.CELL_SIZE.height), (x + Game.CELL_SIZE.width, y), ], ) def draw_board_border(self): bwidth = (Game.BOARD_SIZE.width + 2) * Game.CELL_SIZE.width bheight = Game.WINDOW_SIZE.height for x in range(Game.BOARD_SIZE.width + 2): self.draw_block(x * Game.CELL_SIZE.width, 0, "gray") self.draw_block( x * Game.CELL_SIZE.width, bheight - Game.CELL_SIZE.height, "gray", ) for y in range(1, Game.BOARD_SIZE.height + 1): self.draw_block(0, y * game.CELL_SIZE.height, "gray") self.draw_block( bwidth - Game.CELL_SIZE.width, y * game.CELL_SIZE.height, "gray", ) def draw_board_content(self): for x in range(Game.BOARD_SIZE.width): for y in range(Game.BOARD_SIZE.height): if self.board[y][x]: self.draw_block( (x + 1) * Game.CELL_SIZE.width, (y + 1) * Game.CELL_SIZE.height, self.board[y][x], ) def draw_current_block(self): start_x, start_y = self.current_block_pos for x in range(self.current_block.width): for y in range(self.current_block.height): if self.current_block.shape[y][x]: screen_x = (start_x + x + 1) * Game.CELL_SIZE.width screen_y = (start_y + y + 1) * Game.CELL_SIZE.height self.draw_block( screen_x, screen_y, self.current_block.color ) def frame_time(self) -> float: ticks = self.clock.get_time() if not ticks: return 1 / Game.FRAMERATE else: return ticks / 1000 def handle_key_event(self, typ, key): if typ == pygame.KEYDOWN: if key in self.action_map: self.pending_actions.append(self.action_map[key].type) self.key_states[key] = True elif typ == pygame.KEYUP: self.key_states[key] = False def handle_events(self): # process already pushed keys for key, state in self.key_states.items(): if ( state and key in self.action_map and self.action_map[key].repeat ): self.pending_actions.append(self.action_map[key].type) # now process new events for event in pygame.event.get(): match event.type: case pygame.QUIT: self.running = False case pygame.KEYDOWN | pygame.KEYUP: self.handle_key_event(event.type, event.key) def swap_with_hold(self): to_swap = self.held_piece used_next = False if not to_swap: to_swap = self.next_piece used_next = True if not self.intersects_board(*self.current_block_pos, to_swap): self.held_piece = self.current_block self.current_block = to_swap if used_next: self.next_piece = self.random_tetromino() def intersects_board(self, x: int, y: int, piece: Tetromino): pw, ph = piece.width, piece.height if x < 0 or y < 0: return True elif x + pw > Game.BOARD_SIZE.width or y + ph > Game.BOARD_SIZE.height: return True shape = piece.shape for cy, row in enumerate(shape): for cx, cell in enumerate(row): if cell and self.board[y + cy][x + cx]: return True return False def rotate_current_piece(self, times: int): new_piece = self.current_block.rotated(times) cx, cy = self.current_block_pos for dx in [0, -1]: for dy in [1, 0, -1]: np = Cell(cx + dx, cy + dy) if not self.intersects_board(*np, new_piece): self.current_block = new_piece self.current_block_pos = np # give the user some leeway to splin many times self.subcell_drop = 0 return def move_current_piece(self, dir: Direction): dx = dir * Game.MOVE_SPEED * self.frame_time() self.subcell_move += dx move_cell = int(self.subcell_move) self.subcell_move -= move_cell # give the user some leeway to move in tight quarters self.subcell_drop = 0 if move_cell: new_pos = Cell( self.current_block_pos.x + move_cell, self.current_block_pos.y ) if not self.intersects_board(*new_pos, self.current_block): self.current_block_pos = new_pos def process_game_actions(self): if not ( {Action.Type.MOVE_LEFT, Action.Type.MOVE_RIGHT} & set(self.pending_actions) ): self.subcell_move = 0 if Action.Type.DROP not in self.pending_actions: self.stop_drop = False while self.pending_actions: match self.pending_actions.pop(0): case Action.Type.ROTATE_LEFT: self.rotate_current_piece(Direction.LEFT) case Action.Type.ROTATE_RIGHT: self.rotate_current_piece(Direction.RIGHT) case Action.Type.SWAP_HOLD: self.swap_with_hold() case Action.Type.DROP if not self.stop_drop: self.doing_drop = True case Action.Type.MOVE_LEFT: self.move_current_piece(Direction.LEFT) case Action.Type.MOVE_RIGHT: self.move_current_piece(Direction.RIGHT) case Action.Type.RESTART: self.start_new_game() self.pending_actions = [] def place_piece(self, x: int, y: int, piece: Tetromino): self.score += Game.PLACE_POINTS for dy, row in enumerate(piece.shape): for dx, cell in enumerate(row): if cell: self.board[y + dy][x + dx] = piece.color # prevent user from accidentally dropping next piece too self.stop_drop = True def advance_piece(self): speed = ( Game.FAST_DROP_SPEED if self.doing_drop else Game.NORMAL_DROP_SPEED ) self.subcell_drop += speed * self.frame_time() move_cell = int(self.subcell_drop) self.subcell_drop -= move_cell cp = self.current_block_pos if not self.intersects_board( cp.x, cp.y + move_cell, self.current_block ): self.current_block_pos = Cell(cp.x, cp.y + move_cell) else: for dy in range(move_cell, -1, -1): if not self.intersects_board( cp.x, cp.y + dy, self.current_block ): self.place_piece(cp.x, cp.y + dy, self.current_block) self.current_block = None return self.game_over = True @staticmethod def generate_clear_cells(): bw = Game.BOARD_SIZE.width if bw % 2 == 0: return zip(range(bw // 2 - 1, -1, -1), range(bw // 2, bw)) else: return zip(range(bw // 2, -1, -1), range(bw // 2, bw)) def maybe_clear_rows(self): def do_single_clear(y: int): row = self.board[y] for x1, x2 in Game.generate_clear_cells(): if row[x1]: row[x1] = None row[x2] = None break def drop_rows_above_for_clear(y: int): self.board.pop(y) # this also restores indices for future clears self.board.insert(0, [None] * Game.BOARD_SIZE.width) if not self.clearing_rows: for y, row in enumerate(self.board): if all(row): self.score += self.CLEAR_POINTS self.clearing_rows.add(y) elif not any(self.board[next(iter(self.clearing_rows))]): # Order matters here for y in sorted(self.clearing_rows): drop_rows_above_for_clear(y) self.clearing_rows = set() self.clearing_frames = self.CLEAR_SPEED else: if self.clearing_frames == self.CLEAR_SPEED: self.clearing_frames = 0 for y in self.clearing_rows: do_single_clear(y) else: self.clearing_frames += 1 def draw_score_and_instructions(self): text = ( "Press \nfor help.\n" "\n" f"Score: {self.score:05}\n" f" HS: {self.high_score:05}" ) exts = self.TINY_FONT.compute_extents(text) x = Game.CONTROLS_START_X + Game.CONTROL_DISPLAY_BORDER y = ( self.screen.get_size()[1] - Game.CONTROL_DISPLAY_BORDER - exts.height ) self.TINY_FONT.render_text(text, self.screen, x, y, "white") @cache def get_scaled_piece( self, piece: Tetromino, scale: float ) -> pygame.surface.Surface: ow = piece.width * Game.CELL_SIZE.width oh = piece.height * Game.CELL_SIZE.height os_surf = pygame.surface.Surface((ow, oh), pygame.SRCALPHA) for y, row in enumerate(piece.shape): for x, cell in enumerate(row): if cell: self.draw_block( x * Game.CELL_SIZE.width, y * Game.CELL_SIZE.height, piece.color, os_surf, ) sw, sh = int(ow * scale), int(oh * scale) return pygame.transform.scale(os_surf, (sw, sh)) def draw_boxed_piece( self, text: str, piece: Tetromino | None, x: int, y: int, box_size: int, ) -> Size: """Return the overall bounding box.""" text_ext = Game.TINY_FONT.compute_extents(text) Game.TINY_FONT.center_text( text, self.screen, x + box_size // 2, y + text_ext.height // 2, "white", ) pygame.draw.rect( self.screen, Game.PALETTE["gray"].norm, ( x, y + text_ext.height + Game.TINY_FONT.row_kern, box_size, box_size, ), width=Game.INFO_BOX_BORDER_SIZE, ) if piece: piece_surf = self.get_scaled_piece(piece, 0.75) pw, ph = piece_surf.get_size() self.screen.blit( piece_surf, ( x + box_size // 2 - pw // 2, y + text_ext.height + Game.TINY_FONT.row_kern + box_size // 2 - ph // 2, ), ) return Size( max(box_size, text_ext.width), text_ext.height + box_size + Game.TINY_FONT.row_kern, ) def draw_held_and_next_piece(self): width = int(Game.CONTROLS_WIDTH * 0.8) y = Game.CONTROL_DISPLAY_BORDER x = Game.CONTROLS_START_X + Game.CONTROLS_WIDTH // 2 - width // 2 exts = self.draw_boxed_piece("Hold", self.held_piece, x, y, width) self.draw_boxed_piece( "Next", self.next_piece, x, y + exts.height + 2 * Game.CONTROL_DISPLAY_BORDER, width, ) def process_and_draw_help_overlay(self): self.draw_gray_overlay() hs = Game.make_help_string(Game.GAME_ACTION_MAP) exts = Game.TINY_FONT.compute_extents(hs) sw, sh = self.screen.get_size() tot_border = Game.INFO_BOX_BORDER_SIZE + Game.HELP_MENU_INSET box_x = sw // 2 - exts.width // 2 - tot_border box_y = sh // 2 - exts.height // 2 - tot_border box_w = exts.width + 2 * tot_border box_h = exts.height + 2 * tot_border pygame.draw.rect(self.screen, "black", (box_x, box_y, box_w, box_h)) pygame.draw.rect( self.screen, Game.PALETTE["gray"].norm, (box_x, box_y, box_w, box_h), width=Game.INFO_BOX_BORDER_SIZE, ) Game.TINY_FONT.center_text(hs, self.screen, sw // 2, sh // 2, "white") if Action.Type.CLOSE_HELP in self.pending_actions: self.pending_actions = [] self.action_map = Game.GAME_ACTION_MAP self.help_mode = False def game_loop(self): self.doing_drop = False self.maybe_clear_rows() if Action.Type.OPEN_HELP in self.pending_actions: # do no matter what self.help_mode = True self.action_map = Game.HELP_ACTION_MAP elif not self.clearing_rows: self.process_game_actions() self.advance_piece() else: # ignore input while clearing self.pending_actions = [] if not self.current_block: self.swap_in_next_piece() self.screen.fill("black") if not self.clearing_rows: self.draw_current_block() self.draw_board_content() self.draw_board_border() self.draw_held_and_next_piece() self.draw_score_and_instructions() if self.help_mode: self.process_and_draw_help_overlay() def start_new_game(self): self.cur_random_peice_seq = None self.clear_board() self.next_piece = None self.swap_in_next_piece(True) self.held_piece = None self.subcell_move = 0 self.subcell_drop = 0 self.clearing_rows = set() # immediately clear the first block self.clearing_frames = Game.CLEAR_SPEED self.score = 0 self.held_piece = None self.game_over = False def handle_game_over_actions(self): while self.pending_actions: act = self.pending_actions.pop(0) if act == Action.Type.RESTART: self.start_new_game() def make_gray_overlay(self): s = pygame.surface.Surface(self.screen.get_size(), pygame.SRCALPHA) pygame.draw.rect(s, "#44444466", (0, 0, *s.get_size())) return s def draw_gray_overlay(self): self.screen.blit(self.gray_overlay, (0, 0)) def game_over_loop(self): self.handle_game_over_actions() if not self.game_over: return self.screen.fill("black") self.draw_board_content() self.draw_current_block() self.draw_board_border() self.draw_held_and_next_piece() self.draw_score_and_instructions() self.draw_gray_overlay() sw, sh = self.screen.get_size() exts = self.FONT.compute_extents("Game Over!") score_text = f"Score: {self.score:05}" exts2 = self.SMALL_FONT.compute_extents(score_text) exts3 = self.SMALL_FONT.compute_extents("Press to retry.") text_width = max(exts.width, exts2.width, exts3.width) text_height = ( exts.height + exts2.height + exts3.height + 2 * self.FONT.row_kern ) tot_border = Game.INFO_BOX_BORDER_SIZE + Game.HELP_MENU_INSET box_x = sw // 2 - text_width // 2 - tot_border box_y = sh // 2 - text_height // 2 - tot_border box_w = text_width + 2 * tot_border box_h = text_height + 2 * tot_border pygame.draw.rect(self.screen, "black", (box_x, box_y, box_w, box_h)) pygame.draw.rect( self.screen, Game.PALETTE["gray"].norm, (box_x, box_y, box_w, box_h), width=Game.INFO_BOX_BORDER_SIZE, ) self.FONT.render_text( "Game Over!", self.screen, sw // 2 - exts.width // 2, sh // 2 - text_height // 2, "white", ) self.SMALL_FONT.render_text( score_text, self.screen, sw // 2 - exts2.width // 2, sh // 2 - text_height // 2 + exts.height + self.FONT.row_kern, "white", ) self.SMALL_FONT.render_text( "Press to retry.", self.screen, sw // 2 - exts3.width // 2, sh // 2 - text_height // 2 + exts.height + exts2.height + 2 * self.FONT.row_kern, "white", ) def loop(self): self.running = True while self.running: self.handle_events() if self.game_over: self.game_over_loop() else: self.game_loop() pygame.display.flip() self.clock.tick(Game.FRAMERATE) def swap_in_next_piece(self, force: bool = False): if not self.next_piece or force: self.next_piece = self.random_tetromino() self.current_block = self.random_tetromino() else: self.current_block = self.next_piece self.next_piece = self.random_tetromino() # top left corner self.current_block_pos = Cell( self.BOARD_SIZE.width // 2 - math.ceil(self.current_block.width / 2), 0, ) def clear_board(self): self.board = make_matrix(*Game.BOARD_SIZE) # not in __init__ to facilitate debugging in ipython def init(self): pygame.init() self.screen = pygame.display.set_mode( Game.WINDOW_SIZE, pygame.SCALED | pygame.HWACCEL ) pygame.display.set_caption("Tetris") self.clock = pygame.time.Clock() self.gray_overlay = self.make_gray_overlay() pygame.key.stop_text_input() self.key_states = defaultdict(lambda: False) self.pending_actions = [] self.stop_drop = False self.help_mode = False self.high_score = 0 self.start_new_game() def run(self): self.init() self.loop() GLYPH_DATA = [ Font.Glyph( " ", [ " ", " ", " ", " ", " ", ], ), Font.Glyph( "a", [ "*****", "* *", "*****", "* *", "* *", ], ), Font.Glyph( "b", [ "**** ", "* *", "*****", "* *", "**** ", ], ), Font.Glyph( "c", [ "*****", "* *", "* ", "* *", "*****", ], ), Font.Glyph( "d", [ "**** ", "* *", "* *", "* *", "**** ", ], ), Font.Glyph( "e", [ "*****", "* ", "**** ", "* ", "*****", ], ), Font.Glyph( "f", [ "*****", "* ", "**** ", "* ", "* ", ], ), Font.Glyph( "g", [ "*****", "* ", "* **", "* *", "*****", ], ), Font.Glyph( "h", [ "* *", "* *", "*****", "* *", "* *", ], ), Font.Glyph( "i", [ "*****", " * ", " * ", " * ", "*****", ], ), Font.Glyph( "j", [ "*****", " * ", " * ", "* * ", " ** ", ], ), Font.Glyph( "k", [ "* *", "* * ", "*** ", "* * ", "* *", ], ), Font.Glyph( "l", [ "* ", "* ", "* ", "* ", "*****", ], ), Font.Glyph( "m", [ "* *", "** **", "* * *", "* *", "* *", ], ), Font.Glyph( "n", [ "* *", "** *", "* * *", "* **", "* *", ], ), Font.Glyph( "o", [ " *** ", "* *", "* *", "* *", " *** ", ], ), Font.Glyph( "p", [ "**** ", "* *", "**** ", "* ", "* ", ], ), Font.Glyph( "q", [ " *** ", "* *", "* * *", "* * ", " ** *", ], ), Font.Glyph( "r", [ "**** ", "* *", "**** ", "* * ", "* *", ], ), Font.Glyph( "s", [ " ****", "* ", " *** ", " *", "**** ", ], ), Font.Glyph( "t", [ "*****", " * ", " * ", " * ", " * ", ], ), Font.Glyph( "u", [ "* *", "* *", "* *", "* *", " *** ", ], ), Font.Glyph( "v", [ "* *", "* *", "* *", " * * ", " * ", ], ), Font.Glyph( "w", [ "* *", "* *", "* * *", "** **", "* *", ], ), Font.Glyph( "x", [ "* *", " * * ", " * ", " * * ", "* *", ], ), Font.Glyph( "y", [ "* *", " * * ", " * ", " * ", " * ", ], ), Font.Glyph( "z", [ "*****", " * ", " * ", " * ", "*****", ], ), Font.Glyph( "0", [ " *** ", "* *", "* * *", "* *", " *** ", ], ), Font.Glyph( "1", [ " ** ", "* * ", " * ", " * ", "*****", ], ), Font.Glyph( "2", [ " *** ", "* *", " * ", " * ", " ****", ], ), Font.Glyph( "3", [ " *** ", "* *", " ***", "* *", " *** ", ], ), Font.Glyph( "4", [ "* * ", "* * ", "*****", " * ", " * ", ], ), Font.Glyph( "5", [ "*****", "* ", "**** ", " *", "**** ", ], ), Font.Glyph( "6", [ " ****", "* ", "**** ", "* *", " *** ", ], ), Font.Glyph( "7", [ "*****", " * ", " * ", " * ", "* ", ], ), Font.Glyph( "8", [ " *** ", "* *", " *** ", "* *", " *** ", ], ), Font.Glyph( "9", [ " *** ", "* *", " ****", " *", "**** ", ], ), Font.Glyph( ".", [ " ", " ", " ", " ", " *", ], ), Font.Glyph( "!", [ " *", " *", " *", " ", " *", ], ), Font.Glyph( ":", [ " ", " *", " ", " *", " ", ], ), Font.Glyph( "<", [ " **", " ** ", "* ", " ** ", " **", ], ), Font.Glyph( ">", [ "** ", " ** ", " *", " ** ", "** ", ], ), ] if __name__ == "__main__": game = Game() game.run()