Update a bunch of stuff
This commit is contained in:
@@ -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):
|
||||||
|
# some shapes jut out of the board, prevent that from crashing
|
||||||
|
try:
|
||||||
if cell and self.board[y + cy][x + cx]:
|
if cell and self.board[y + cy][x + cx]:
|
||||||
return True
|
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))]):
|
else:
|
||||||
|
if self.clearing_frames == self.CLEAR_SPEED:
|
||||||
|
if not any(self.board[next(iter(self.clearing_rows))]):
|
||||||
# Order matters here
|
# Order matters here
|
||||||
for y in sorted(self.clearing_rows):
|
for y in sorted(self.clearing_rows):
|
||||||
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
|
||||||
else:
|
else:
|
||||||
if self.clearing_frames == self.CLEAR_SPEED:
|
|
||||||
self.clearing_frames = 0
|
|
||||||
for y in self.clearing_rows:
|
for y in self.clearing_rows:
|
||||||
do_single_clear(y)
|
do_single_clear(y)
|
||||||
|
self.clearing_frames = 0
|
||||||
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:
|
||||||
self.process_game_actions()
|
|
||||||
self.advance_piece()
|
|
||||||
else:
|
|
||||||
# ignore input while clearing
|
|
||||||
self.pending_actions = []
|
|
||||||
|
|
||||||
if not self.current_block:
|
if not self.current_block:
|
||||||
self.swap_in_next_piece()
|
self.swap_in_next_piece()
|
||||||
|
|
||||||
|
self.process_game_actions()
|
||||||
|
self.advance_piece()
|
||||||
|
else:
|
||||||
|
self.process_clear_actions()
|
||||||
|
|
||||||
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()
|
||||||
|
|||||||
Reference in New Issue
Block a user