Update a bunch of stuff

This commit is contained in:
2026-05-01 12:34:28 -07:00
parent 09208c3d49
commit c6c5e2720a
+112 -36
View File
@@ -2,7 +2,7 @@ import pygame
import pygame.draw as draw
from collections import namedtuple, defaultdict
from typing import NamedTuple, Any, Iterable, Optional, Callable
from functools import cache
from functools import cache, lru_cache
import random
import math
from enum import Enum, auto
@@ -45,13 +45,53 @@ class Tetromino(NamedTuple):
color: str
shape: list[list]
@property
def height(self):
return len(self.shape)
@property
def width(self):
return len(self.shape[0])
@property
def height(self):
return len(self.shape)
@lru_cache(maxsize=16)
def x_adj(self):
def clz(arr: list):
for i, c in enumerate(arr):
if c:
return i
return len(arr)
return min(map(clz, self.shape))
@property
@lru_cache(maxsize=16)
def y_adj(self):
for y, row in enumerate(self.shape):
for x, cell in enumerate(row):
if cell:
return y
return self.height
@property
@lru_cache(maxsize=16)
def bounding_box_width(self):
def ctz(arr: list):
for i, c in enumerate(reversed(arr)):
if c:
return i
return len(arr)
return max(map(lambda r: len(r) - ctz(r), self.shape))
@property
@lru_cache(maxsize=16)
def bounding_box_height(self):
for y, row in reversed(list(enumerate(self.shape))):
for x, cell in enumerate(row):
if cell:
return y + 1
return 0
def rotated(self, times: int):
"""Return self rotated TIMES times to the right."""
@@ -164,7 +204,7 @@ class Font:
self.glyphs,
)
@cache
@lru_cache(maxsize=16)
def compute_extents(self, text: str) -> Size:
text = text.lower()
total_width = 0
@@ -267,7 +307,7 @@ class Game:
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
CLEAR_SPEED = 10 # frames per block
BOARD_SIZE = Size(10, 20) # Cell count
CELL_SIZE = Size(30, 30)
CELL_BORDER_SIZE = CELL_SIZE.width // 10
@@ -288,13 +328,15 @@ class Game:
"green": ColorSet("#00cd00", "#00ff00", "#009a00"),
}
TETROMINOS = [
Tetromino("aqua", [[1, 1, 1, 1]]),
Tetromino(
"aqua", [[0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0]]
),
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]]),
Tetromino("purple", [[0, 0, 0], [1, 1, 1], [0, 1, 0]]),
Tetromino("blue", [[0, 0, 1], [0, 0, 1], [0, 1, 1]]),
Tetromino("orange", [[1, 0, 0], [1, 0, 0], [1, 1, 0]]),
Tetromino("green", [[0, 1, 1], [1, 1, 0], [0, 0, 0]]),
Tetromino("red", [[1, 1, 0], [0, 1, 1], [0, 0, 0]]),
]
GAME_ACTION_MAP = Action.make_map(
Action(
@@ -380,7 +422,7 @@ class Game:
CONTROLS_START_X = (BOARD_SIZE.width + 2) * CELL_SIZE.width
@staticmethod
@cache
@lru_cache(maxsize=1)
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))
@@ -565,23 +607,27 @@ class Game:
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:
pw, ph = piece.bounding_box_width, piece.bounding_box_height
if x + piece.x_adj < 0 or y + piece.y_adj < 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
# some shapes jut out of the board, prevent that from crashing
try:
if cell and self.board[y + cy][x + cx]:
return True
except IndexError:
continue
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]:
for dx in [0, 1, -1]:
for dy in [0, 1, -1]:
np = Cell(cx + dx, cy + dy)
if not self.intersects_board(*np, new_piece):
self.current_block = new_piece
@@ -594,13 +640,22 @@ class Game:
self.subcell_move += dx
move_cell = int(self.subcell_move)
self.subcell_move -= move_cell
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
)
if not self.intersects_board(*new_pos, self.current_block):
self.did_user_move_or_spin = True
self.current_block_pos = new_pos
# test if the user is moving to a valid cell and don't place if they
# are
else:
move_cell = int(math.copysign(1, self.subcell_move))
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.did_user_move_or_spin = True
def process_game_actions(self):
if not (
@@ -669,7 +724,7 @@ class Game:
cp.x, cp.y + move_cell, self.current_block
):
self.current_block_pos = Cell(cp.x, cp.y + move_cell)
# don't place the pice if the user moved it
# don't place the piece 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(
@@ -708,17 +763,18 @@ class Game:
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:
if 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:
for y in self.clearing_rows:
do_single_clear(y)
self.clearing_frames = 0
for y in self.clearing_rows:
do_single_clear(y)
else:
self.clearing_frames += 1
@@ -738,9 +794,9 @@ class Game:
)
self.TINY_FONT.render_text(text, self.screen, x, y, "white")
@cache
@lru_cache(maxsize=4)
def get_scaled_piece(
self, piece: Tetromino, scale: float
self, piece: Tetromino, scale: float | int, alpha: int = 255
) -> pygame.surface.Surface:
ow = piece.width * Game.CELL_SIZE.width
oh = piece.height * Game.CELL_SIZE.height
@@ -755,6 +811,8 @@ class Game:
os_surf,
)
sw, sh = int(ow * scale), int(oh * scale)
if alpha != 255:
os_surf.set_alpha(alpha)
return pygame.transform.scale(os_surf, (sw, sh))
def draw_boxed_piece(
@@ -846,6 +904,24 @@ class Game:
self.action_map = Game.GAME_ACTION_MAP
self.help_mode = False
def draw_ghost_piece(self):
x, y = self.current_block_pos
y += self.cells_for_hard_drop()
surf = self.get_scaled_piece(self.current_block, 1, 0x80)
self.screen.blit(
surf,
((x + 1) * Game.CELL_SIZE.width, y * Game.CELL_SIZE.height),
)
def process_clear_actions(self):
# allow rotating the next piece during clear
while self.pending_actions:
match self.pending_actions.pop(0):
case Action.Type.ROTATE_LEFT:
self.next_piece = self.next_piece.rotated(-1)
case Action.Type.ROTATE_RIGHT:
self.next_piece = self.next_piece.rotated(1)
def game_loop(self):
self.soft_drop = False
self.hard_drop = False
@@ -857,18 +933,18 @@ class Game:
self.help_mode = True
self.action_map = Game.HELP_ACTION_MAP
elif not self.clearing_rows:
if not self.current_block:
self.swap_in_next_piece()
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.process_clear_actions()
self.screen.fill("black")
if not self.clearing_rows:
if self.current_block:
self.draw_current_block()
self.draw_ghost_piece()
self.draw_board_content()
self.draw_board_border()
self.draw_held_and_next_piece()