From 09208c3d49733783e5ef8184da489f15d8978864 Mon Sep 17 00:00:00 2001 From: Alexander Rosenberg Date: Fri, 1 May 2026 05:15:05 -0700 Subject: [PATCH] Hard drops and fix moving --- tetris.py | 87 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 64 insertions(+), 23 deletions(-) diff --git a/tetris.py b/tetris.py index 66a15fe..ea0cfea 100644 --- a/tetris.py +++ b/tetris.py @@ -96,7 +96,8 @@ class Action(NamedTuple): # In Game ROTATE_LEFT = auto() ROTATE_RIGHT = auto() - DROP = auto() + SOFT_DROP = auto() + HARD_DROP = auto() SWAP_HOLD = auto() MOVE_LEFT = auto() MOVE_RIGHT = auto() @@ -259,11 +260,13 @@ class LazyVariable: class Game: - PLACE_POINTS = 10 + NORMAL_PLACE_POINTS = 10 + HARD_PLACE_POINTS = 20 CLEAR_POINTS = 100 MOVE_SPEED = 20 # cells per second NORMAL_DROP_SPEED = 3 # cells per second FAST_DROP_SPEED = 30 # cells per second + MOVED_PROP_SPEED = 1 # cells per second CLEAR_SPEED = 5 # frames per block BOARD_SIZE = Size(10, 20) # Cell count CELL_SIZE = Size(30, 30) @@ -307,13 +310,22 @@ class Game: "Rotate the current piece right.", ), Action( - pygame.K_s, + pygame.K_c, Action.Type.SWAP_HOLD, False, "Swap the current and held pieces.", ), Action( - pygame.K_DOWN, Action.Type.DROP, True, "Drop the current piece." + pygame.K_DOWN, + Action.Type.SOFT_DROP, + True, + "Soft drop the current piece.", + ), + Action( + pygame.K_SPACE, + Action.Type.HARD_DROP, + True, + "Hard drop the current piece.", ), Action( pygame.K_LEFT, @@ -537,6 +549,8 @@ class Game: self.handle_key_event(event.type, event.key) def swap_with_hold(self): + if self.did_swap: + return to_swap = self.held_piece used_next = False if not to_swap: @@ -544,6 +558,8 @@ class Game: used_next = True if not self.intersects_board(*self.current_block_pos, to_swap): self.held_piece = self.current_block + self.move_piece_back_to_top() + self.did_swap = True self.current_block = to_swap if used_next: self.next_piece = self.random_tetromino() @@ -570,8 +586,7 @@ class Game: 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 + self.did_user_move_or_spin = True return def move_current_piece(self, dir: Direction): @@ -579,8 +594,7 @@ class Game: 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 + self.did_user_move_or_spin = True if move_cell: new_pos = Cell( self.current_block_pos.x + move_cell, self.current_block_pos.y @@ -594,7 +608,10 @@ class Game: & set(self.pending_actions) ): self.subcell_move = 0 - if Action.Type.DROP not in self.pending_actions: + if not ( + {Action.Type.SOFT_DROP, Action.Type.HARD_DROP} + & set(self.pending_actions) + ): self.stop_drop = False while self.pending_actions: match self.pending_actions.pop(0): @@ -604,8 +621,10 @@ class Game: 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.SOFT_DROP if not self.stop_drop: + self.soft_drop = True + case Action.Type.HARD_DROP if not self.stop_drop: + self.hard_drop = True case Action.Type.MOVE_LEFT: self.move_current_piece(Direction.LEFT) case Action.Type.MOVE_RIGHT: @@ -615,33 +634,50 @@ class Game: self.pending_actions = [] def place_piece(self, x: int, y: int, piece: Tetromino): - self.score += Game.PLACE_POINTS + if self.hard_drop: + self.score += Game.HARD_PLACE_POINTS + else: + self.score += Game.NORMAL_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 + self.subcell_drop = 0 + + def cells_for_hard_drop(self): + cx, cy = self.current_block_pos + dy = 0 + while not self.intersects_board(cx, cy + dy, self.current_block): + dy += 1 + return dy def advance_piece(self): - speed = ( - Game.FAST_DROP_SPEED if self.doing_drop else Game.NORMAL_DROP_SPEED - ) + speed = Game.NORMAL_DROP_SPEED + if self.did_user_move_or_spin: + speed = Game.MOVED_PROP_SPEED + elif self.soft_drop: + speed = Game.FAST_DROP_SPEED self.subcell_drop += speed * self.frame_time() move_cell = int(self.subcell_drop) self.subcell_drop -= move_cell + if self.hard_drop: + move_cell = self.cells_for_hard_drop() 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: + # don't place the pice if the user moved it + elif not self.did_user_move_or_spin: 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 + self.did_swap = False return self.game_over = True @@ -811,7 +847,9 @@ class Game: self.help_mode = False def game_loop(self): - self.doing_drop = False + self.soft_drop = False + self.hard_drop = False + self.did_user_move_or_spin = False self.maybe_clear_rows() if Action.Type.OPEN_HELP in self.pending_actions: @@ -846,6 +884,7 @@ class Game: self.next_piece = None self.swap_in_next_piece(True) self.held_piece = None + self.did_swap = False self.subcell_move = 0 self.subcell_drop = 0 self.clearing_rows = set() @@ -945,6 +984,13 @@ class Game: pygame.display.flip() self.clock.tick(Game.FRAMERATE) + def move_piece_back_to_top(self): + self.current_block_pos = Cell( + self.BOARD_SIZE.width // 2 + - math.ceil(self.current_block.width / 2), + 0, + ) + def swap_in_next_piece(self, force: bool = False): if not self.next_piece or force: self.next_piece = self.random_tetromino() @@ -952,12 +998,7 @@ class Game: 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, - ) + self.move_piece_back_to_top() def clear_board(self): self.board = make_matrix(*Game.BOARD_SIZE)