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 import pygame.draw as draw
from collections import namedtuple, defaultdict from collections import namedtuple, defaultdict
from typing import NamedTuple, Any, Iterable, Optional, Callable from typing import NamedTuple, Any, Iterable, Optional, Callable
from functools import cache from functools import cache, lru_cache
import random import random
import math import math
from enum import Enum, auto from enum import Enum, auto
@@ -45,13 +45,53 @@ class Tetromino(NamedTuple):
color: str color: str
shape: list[list] shape: list[list]
@property
def height(self):
return len(self.shape)
@property @property
def width(self): def width(self):
return len(self.shape[0]) return len(self.shape[0])
@property @property
def height(self): @lru_cache(maxsize=16)
return len(self.shape) 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): def rotated(self, times: int):
"""Return self rotated TIMES times to the right.""" """Return self rotated TIMES times to the right."""
@@ -164,7 +204,7 @@ class Font:
self.glyphs, self.glyphs,
) )
@cache @lru_cache(maxsize=16)
def compute_extents(self, text: str) -> Size: def compute_extents(self, text: str) -> Size:
text = text.lower() text = text.lower()
total_width = 0 total_width = 0
@@ -267,7 +307,7 @@ class Game:
NORMAL_DROP_SPEED = 3 # cells per second NORMAL_DROP_SPEED = 3 # cells per second
FAST_DROP_SPEED = 30 # cells per second FAST_DROP_SPEED = 30 # cells per second
MOVED_PROP_SPEED = 1 # 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 BOARD_SIZE = Size(10, 20) # Cell count
CELL_SIZE = Size(30, 30) CELL_SIZE = Size(30, 30)
CELL_BORDER_SIZE = CELL_SIZE.width // 10 CELL_BORDER_SIZE = CELL_SIZE.width // 10
@@ -288,13 +328,15 @@ class Game:
"green": ColorSet("#00cd00", "#00ff00", "#009a00"), "green": ColorSet("#00cd00", "#00ff00", "#009a00"),
} }
TETROMINOS = [ 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("yellow", [[1, 1], [1, 1]]),
Tetromino("purple", [[1, 1, 1], [0, 1, 0]]), Tetromino("purple", [[0, 0, 0], [1, 1, 1], [0, 1, 0]]),
Tetromino("blue", [[0, 1], [0, 1], [1, 1]]), Tetromino("blue", [[0, 0, 1], [0, 0, 1], [0, 1, 1]]),
Tetromino("orange", [[1, 0], [1, 0], [1, 1]]), Tetromino("orange", [[1, 0, 0], [1, 0, 0], [1, 1, 0]]),
Tetromino("green", [[0, 1, 1], [1, 1, 0]]), Tetromino("green", [[0, 1, 1], [1, 1, 0], [0, 0, 0]]),
Tetromino("red", [[1, 1, 0], [0, 1, 1]]), Tetromino("red", [[1, 1, 0], [0, 1, 1], [0, 0, 0]]),
] ]
GAME_ACTION_MAP = Action.make_map( GAME_ACTION_MAP = Action.make_map(
Action( Action(
@@ -380,7 +422,7 @@ class Game:
CONTROLS_START_X = (BOARD_SIZE.width + 2) * CELL_SIZE.width CONTROLS_START_X = (BOARD_SIZE.width + 2) * CELL_SIZE.width
@staticmethod @staticmethod
@cache @lru_cache(maxsize=1)
def make_help_string(am: dict[int, Action]): def make_help_string(am: dict[int, Action]):
key_names = [pygame.key.name(k) for k in am.keys()] key_names = [pygame.key.name(k) for k in am.keys()]
max_key_len = max(map(len, key_names)) max_key_len = max(map(len, key_names))
@@ -565,23 +607,27 @@ class Game:
self.next_piece = self.random_tetromino() 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.bounding_box_width, piece.bounding_box_height
if x < 0 or y < 0: if x + piece.x_adj < 0 or y + piece.y_adj < 0:
return True return True
elif x + pw > Game.BOARD_SIZE.width or y + ph > Game.BOARD_SIZE.height: elif x + pw > Game.BOARD_SIZE.width or y + ph > Game.BOARD_SIZE.height:
return True return True
shape = piece.shape shape = piece.shape
for cy, row in enumerate(shape): for cy, row in enumerate(shape):
for cx, cell in enumerate(row): for cx, cell in enumerate(row):
if cell and self.board[y + cy][x + cx]: # some shapes jut out of the board, prevent that from crashing
return True try:
if cell and self.board[y + cy][x + cx]:
return True
except IndexError:
continue
return False return False
def rotate_current_piece(self, times: int): def rotate_current_piece(self, times: int):
new_piece = self.current_block.rotated(times) new_piece = self.current_block.rotated(times)
cx, cy = self.current_block_pos cx, cy = self.current_block_pos
for dx in [0, -1]: for dx in [0, 1, -1]:
for dy in [1, 0, -1]: for dy in [0, 1, -1]:
np = Cell(cx + dx, cy + dy) np = Cell(cx + dx, cy + dy)
if not self.intersects_board(*np, new_piece): if not self.intersects_board(*np, new_piece):
self.current_block = new_piece self.current_block = new_piece
@@ -594,13 +640,22 @@ class Game:
self.subcell_move += dx self.subcell_move += dx
move_cell = int(self.subcell_move) move_cell = int(self.subcell_move)
self.subcell_move -= move_cell self.subcell_move -= move_cell
self.did_user_move_or_spin = True
if move_cell: if move_cell:
new_pos = Cell( new_pos = Cell(
self.current_block_pos.x + move_cell, self.current_block_pos.y self.current_block_pos.x + move_cell, self.current_block_pos.y
) )
if not self.intersects_board(*new_pos, self.current_block): if not self.intersects_board(*new_pos, self.current_block):
self.did_user_move_or_spin = True
self.current_block_pos = new_pos 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): def process_game_actions(self):
if not ( if not (
@@ -669,7 +724,7 @@ class Game:
cp.x, cp.y + move_cell, self.current_block cp.x, cp.y + move_cell, self.current_block
): ):
self.current_block_pos = Cell(cp.x, cp.y + move_cell) 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: elif not self.did_user_move_or_spin:
for dy in range(move_cell, -1, -1): for dy in range(move_cell, -1, -1):
if not self.intersects_board( if not self.intersects_board(
@@ -708,17 +763,18 @@ class Game:
if all(row): if all(row):
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))]):
# 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: else:
if self.clearing_frames == self.CLEAR_SPEED: 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 self.clearing_frames = 0
for y in self.clearing_rows:
do_single_clear(y)
else: else:
self.clearing_frames += 1 self.clearing_frames += 1
@@ -738,9 +794,9 @@ class Game:
) )
self.TINY_FONT.render_text(text, self.screen, x, y, "white") self.TINY_FONT.render_text(text, self.screen, x, y, "white")
@cache @lru_cache(maxsize=4)
def get_scaled_piece( def get_scaled_piece(
self, piece: Tetromino, scale: float self, piece: Tetromino, scale: float | int, alpha: int = 255
) -> pygame.surface.Surface: ) -> pygame.surface.Surface:
ow = piece.width * Game.CELL_SIZE.width ow = piece.width * Game.CELL_SIZE.width
oh = piece.height * Game.CELL_SIZE.height oh = piece.height * Game.CELL_SIZE.height
@@ -755,6 +811,8 @@ class Game:
os_surf, os_surf,
) )
sw, sh = int(ow * scale), int(oh * scale) sw, sh = int(ow * scale), int(oh * scale)
if alpha != 255:
os_surf.set_alpha(alpha)
return pygame.transform.scale(os_surf, (sw, sh)) return pygame.transform.scale(os_surf, (sw, sh))
def draw_boxed_piece( def draw_boxed_piece(
@@ -846,6 +904,24 @@ class Game:
self.action_map = Game.GAME_ACTION_MAP self.action_map = Game.GAME_ACTION_MAP
self.help_mode = False 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): def game_loop(self):
self.soft_drop = False self.soft_drop = False
self.hard_drop = False self.hard_drop = False
@@ -857,18 +933,18 @@ class Game:
self.help_mode = True self.help_mode = True
self.action_map = Game.HELP_ACTION_MAP self.action_map = Game.HELP_ACTION_MAP
elif not self.clearing_rows: elif not self.clearing_rows:
if not self.current_block:
self.swap_in_next_piece()
self.process_game_actions() self.process_game_actions()
self.advance_piece() self.advance_piece()
else: else:
# ignore input while clearing self.process_clear_actions()
self.pending_actions = []
if not self.current_block:
self.swap_in_next_piece()
self.screen.fill("black") self.screen.fill("black")
if not self.clearing_rows: if self.current_block:
self.draw_current_block() self.draw_current_block()
self.draw_ghost_piece()
self.draw_board_content() self.draw_board_content()
self.draw_board_border() self.draw_board_border()
self.draw_held_and_next_piece() self.draw_held_and_next_piece()