Continue work

This commit is contained in:
2026-05-01 00:03:54 -07:00
parent b8ae985658
commit 5e0ea6e23e
+134 -20
View File
@@ -43,7 +43,7 @@ def mirror(mat: list[list]) -> list[list]:
class Tetromino(NamedTuple): class Tetromino(NamedTuple):
color: str color: str
shape: list[list[str]] shape: list[list]
@property @property
def width(self): def width(self):
@@ -73,6 +73,12 @@ class Tetromino(NamedTuple):
out.append("\n") out.append("\n")
return "".join(out) return "".join(out)
def __eq__(self, other):
return id(self) == id(other)
def __hash__(self):
return id(self)
class Action(NamedTuple): class Action(NamedTuple):
key: int key: int
@@ -716,13 +722,10 @@ class Game:
) )
SMALL_FONT = FONT.scaled(4, 2, 2) SMALL_FONT = FONT.scaled(4, 2, 2)
TINY_FONT = FONT.scaled(2, 1, 1) TINY_FONT = FONT.scaled(2, 1, 1)
SCORE_DISPLAY_BORDER = 5 CONTROL_DISPLAY_BORDER = 5
NEXT_HELD_BOX_SIZE = 5
CONTROLS_START_X = (BOARD_SIZE.width + 2) * CELL_SIZE.width CONTROLS_START_X = (BOARD_SIZE.width + 2) * CELL_SIZE.width
@staticmethod
def random_tetromino():
return random.choice(Game.TETROMINOS)
def __init__(self): def __init__(self):
self._game_over = False self._game_over = False
@@ -740,15 +743,23 @@ class Game:
else: else:
self.action_map = Game.GAME_ACTION_MAP self.action_map = Game.GAME_ACTION_MAP
def draw_block(self, x: int, y: int, color): 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.""" """Draw a block at (X, Y). Coordinates are for the top left corner."""
if target is None:
target = self.screen
draw.rect( draw.rect(
self.screen, target,
Game.PALETTE[color].norm, Game.PALETTE[color].norm,
(x, y, Game.CELL_SIZE.width, Game.CELL_SIZE.height), (x, y, Game.CELL_SIZE.width, Game.CELL_SIZE.height),
) )
draw.polygon( draw.polygon(
self.screen, target,
Game.PALETTE[color].light, Game.PALETTE[color].light,
[ [
(x, y), (x, y),
@@ -767,7 +778,7 @@ class Game:
], ],
) )
draw.polygon( draw.polygon(
self.screen, target,
Game.PALETTE[color].dark, Game.PALETTE[color].dark,
[ [
(x + Game.CELL_SIZE.width, y), (x + Game.CELL_SIZE.width, y),
@@ -861,10 +872,16 @@ class Game:
self.handle_key_event(event.type, event.key) self.handle_key_event(event.type, event.key)
def swap_with_hold(self): def swap_with_hold(self):
to_swap = self.held_piece or Game.random_tetromino() 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): if not self.intersects_board(*self.current_block_pos, to_swap):
self.held_piece = self.current_block self.held_piece = self.current_block
self.current_block = to_swap self.current_block = to_swap
if used_next:
self.next_piece = self.random_tetromino()
def intersects_board(self, x: int, y: int, piece: Tetromino): def intersects_board(self, x: int, y: int, piece: Tetromino):
pw, ph = piece.width, piece.height pw, ph = piece.width, piece.height
@@ -970,7 +987,8 @@ class Game:
break break
def drop_rows_above_for_clear(y: int): def drop_rows_above_for_clear(y: int):
self.board[: y + 1] = self.board[:y] self.board.pop(y)
# this also restores indices for future clears
self.board.insert(0, [None] * Game.BOARD_SIZE.width) self.board.insert(0, [None] * Game.BOARD_SIZE.width)
if not self.clearing_rows: if not self.clearing_rows:
@@ -979,7 +997,9 @@ class Game:
self.score += self.CLEAR_POINTS self.score += self.CLEAR_POINTS
self.clearing_rows.add(y) self.clearing_rows.add(y)
elif not any(self.board[next(iter(self.clearing_rows))]): elif not any(self.board[next(iter(self.clearing_rows))]):
for y in self.clearing_rows: # Order matters here
for y in sorted(self.clearing_rows):
print(y)
drop_rows_above_for_clear(y) drop_rows_above_for_clear(y)
self.clearing_rows = set() self.clearing_rows = set()
self.clearing_frames = self.CLEAR_SPEED self.clearing_frames = self.CLEAR_SPEED
@@ -994,10 +1014,93 @@ class Game:
def draw_score(self): def draw_score(self):
text = f"Score: {self.score:05}" text = f"Score: {self.score:05}"
exts = self.TINY_FONT.compute_extents(text) exts = self.TINY_FONT.compute_extents(text)
x = Game.CONTROLS_START_X + Game.SCORE_DISPLAY_BORDER x = Game.CONTROLS_START_X + Game.CONTROL_DISPLAY_BORDER
y = self.screen.get_size()[1] - Game.SCORE_DISPLAY_BORDER - exts.height y = (
self.screen.get_size()[1]
- Game.CONTROL_DISPLAY_BORDER
- exts.height
)
self.TINY_FONT.render_text(text, self.screen, x, y, "white") 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): def game_loop(self):
self.doing_drop = False self.doing_drop = False
@@ -1010,19 +1113,21 @@ class Game:
self.pending_actions = [] self.pending_actions = []
if not self.current_block: if not self.current_block:
self.generate_new_block() self.swap_in_next_piece()
self.screen.fill("black") self.screen.fill("black")
if not self.clearing_rows: if not self.clearing_rows:
self.draw_current_block() self.draw_current_block()
self.draw_board_content() self.draw_board_content()
self.draw_board_border() self.draw_board_border()
self.draw_held_and_next_piece()
self.draw_score() self.draw_score()
def start_new_game(self): def start_new_game(self):
self.clear_board() self.clear_board()
self.generate_new_block() self.next_piece = None
self.swap_in_next_piece(True)
self.held_piece = None self.held_piece = None
self.subcell_move = 0 self.subcell_move = 0
self.subcell_drop = 0 self.subcell_drop = 0
@@ -1055,6 +1160,7 @@ class Game:
self.draw_board_content() self.draw_board_content()
self.draw_current_block() self.draw_current_block()
self.draw_board_border() self.draw_board_border()
self.draw_held_and_next_piece()
self.draw_score() self.draw_score()
@@ -1094,8 +1200,14 @@ class Game:
pygame.display.flip() pygame.display.flip()
self.clock.tick(Game.FRAMERATE) self.clock.tick(Game.FRAMERATE)
def generate_new_block(self): def swap_in_next_piece(self, force: bool = False):
self.current_block = Game.random_tetromino() 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 # top left corner
self.current_block_pos = Cell( self.current_block_pos = Cell(
self.BOARD_SIZE.width // 2 self.BOARD_SIZE.width // 2
@@ -1108,7 +1220,9 @@ class Game:
def init(self): def init(self):
pygame.init() pygame.init()
self.screen = pygame.display.set_mode(Game.WINDOW_SIZE, pygame.SCALED) self.screen = pygame.display.set_mode(
Game.WINDOW_SIZE, pygame.SCALED | pygame.HWACCEL
)
pygame.display.set_caption("Tetris") pygame.display.set_caption("Tetris")
self.clock = pygame.time.Clock() self.clock = pygame.time.Clock()