import pygame import pygame.draw as draw from collections import namedtuple, defaultdict from typing import NamedTuple, Any, Iterable, Optional 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 Action(NamedTuple): key: int type: Type repeat: bool desc: str class Type(Enum): # In Game ROTATE_LEFT = auto() ROTATE_RIGHT = auto() DROP = auto() SWAP_HOLD = auto() MOVE_LEFT = auto() MOVE_RIGHT = auto() # Game over RESTART = auto() @staticmethod def make_map(*acts): return {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() multi_row = False total_width = 0 total_height = 0 last_width = 0 last_height = 0 for char in text: if char == "\n": multi_row = True total_width = max(total_width, last_width) last_width = 0 total_height += last_height 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 total_height += last_height return Size(total_width, total_height + (multi_row * self.row_kern)) 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": 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 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 piece and the held piece.", ), 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.", ), ) GAME_OVER_ACTION_MAP = Action.make_map( Action(pygame.K_r, Action.Type.RESTART, False, "Start a new game.") ) FONT = Font( 6, 3, 3, [ 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( ">", [ "** ", " ** ", " *", " ** ", "** ", ], ), ], ) SMALL_FONT = FONT.scaled(4, 2, 2) TINY_FONT = FONT.scaled(2, 1, 1) CONTROL_DISPLAY_BORDER = 5 NEXT_HELD_BOX_SIZE = 5 CONTROLS_START_X = (BOARD_SIZE.width + 2) * CELL_SIZE.width def __init__(self): self._game_over = False @property def game_over(self): return self._game_over @game_over.setter def game_over(self, newval): 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 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 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 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 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: 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) 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 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): print(y) 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(self): text = f"Score: {self.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.NEXT_HELD_BOX_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 game_loop(self): self.doing_drop = False self.maybe_clear_rows() if 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() def start_new_game(self): 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() self.draw_gray_overlay() ss = self.screen.get_size() exts = self.FONT.compute_extents("Game Over!") self.FONT.center_text( "Game Over!", self.screen, ss[0] // 2, ss[1] // 2, "white" ) score_text = f"Score: {self.score:05}" self.SMALL_FONT.center_text( score_text, self.screen, ss[0] // 2, ss[1] // 2 + exts.height + self.FONT.row_kern, "white", ) exts2 = self.SMALL_FONT.compute_extents(score_text) self.SMALL_FONT.center_text( "Press to retry.", self.screen, ss[0] // 2, ss[1] // 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): self.cur_random_peice_seq = None 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) 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.start_new_game() def run(self): self.init() self.loop() if __name__ == "__main__": game = Game() game.run()