Finish initial gameplay stuff

This commit is contained in:
2026-05-01 01:36:04 -07:00
parent 5e0ea6e23e
commit 338fdf2cea
+166 -29
View File
@@ -80,6 +80,17 @@ class Tetromino(NamedTuple):
return id(self) return id(self)
class HashableDict(dict):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
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
type: Type type: Type
@@ -94,13 +105,17 @@ class Action(NamedTuple):
SWAP_HOLD = auto() SWAP_HOLD = auto()
MOVE_LEFT = auto() MOVE_LEFT = auto()
MOVE_RIGHT = auto() MOVE_RIGHT = auto()
OPEN_HELP = auto()
# Game over # Game over
RESTART = auto() RESTART = auto()
# Help
CLOSE_HELP = auto()
@staticmethod @staticmethod
def make_map(*acts): def make_map(*acts):
return {act.key: act for act in acts} return HashableDict({act.key: act for act in acts})
class Font: class Font:
@@ -151,25 +166,29 @@ class Font:
@cache @cache
def compute_extents(self, text: str) -> Size: def compute_extents(self, text: str) -> Size:
text = text.lower() text = text.lower()
multi_row = False
total_width = 0 total_width = 0
total_height = 0 total_height = 0
last_width = 0 last_width = 0
last_height = 0 last_height = 0
for char in text: for char in text:
if char == "\n": if char == "\n":
multi_row = True
total_width = max(total_width, last_width) total_width = max(total_width, last_width)
if last_width == 0:
# empty line
last_height = self.glyphs[" "].height * self.scale
last_width = 0 last_width = 0
total_height += last_height total_height += last_height + self.row_kern
last_height = 0 last_height = 0
else: else:
glyph = self.glyphs[char] glyph = self.glyphs[char]
last_width += glyph.width * self.scale + self.kern last_width += glyph.width * self.scale + self.kern
last_height = max(last_height, glyph.height * self.scale) last_height = max(last_height, glyph.height * self.scale)
total_width = max(last_width, total_width) - self.kern total_width = max(last_width, total_width) - self.kern
if total_width == 0:
# empty line
last_height = self.glyphs[" "].height * self.scale
total_height += last_height total_height += last_height
return Size(total_width, total_height + (multi_row * self.row_kern)) return Size(total_width, total_height)
def render_text( def render_text(
self, text: str, dest: pygame.surface.Surface, x: int, y: int, color self, text: str, dest: pygame.surface.Surface, x: int, y: int, color
@@ -179,6 +198,9 @@ class Font:
max_height = 0 max_height = 0
for char in text: for char in text:
if char == "\n": if char == "\n":
if x == initial_x:
# empty row
max_height = self.glyphs[" "].height * self.scale
y += max_height + self.row_kern y += max_height + self.row_kern
max_height = 0 max_height = 0
x = initial_x x = initial_x
@@ -271,7 +293,7 @@ class Game:
pygame.K_s, pygame.K_s,
Action.Type.SWAP_HOLD, Action.Type.SWAP_HOLD,
False, False,
"Swap the current piece and the held piece.", "Swap the current and held piece.",
), ),
Action( Action(
pygame.K_DOWN, Action.Type.DROP, True, "Drop the current piece." pygame.K_DOWN, Action.Type.DROP, True, "Drop the current piece."
@@ -288,10 +310,30 @@ class Game:
True, True,
"Move the current piece right.", "Move the current piece right.",
), ),
Action(
pygame.K_h,
Action.Type.OPEN_HELP,
False,
"Toggle the help menu.",
),
) )
GAME_OVER_ACTION_MAP = Action.make_map( GAME_OVER_ACTION_MAP = Action.make_map(
Action(pygame.K_r, Action.Type.RESTART, False, "Start a new game.") Action(pygame.K_r, Action.Type.RESTART, False, "Start a new game.")
) )
HELP_ACTION_MAP = Action.make_map(
Action(
pygame.K_ESCAPE,
Action.Type.CLOSE_HELP,
False,
"Close the help menu.",
),
Action(
pygame.K_h,
Action.Type.CLOSE_HELP,
False,
"Close the help menu.",
),
)
FONT = Font( FONT = Font(
6, 6,
@@ -721,20 +763,35 @@ 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, 2, 2)
CONTROL_DISPLAY_BORDER = 5 CONTROL_DISPLAY_BORDER = 5
NEXT_HELD_BOX_SIZE = 5 INFO_BOX_BORDER_SIZE = 5
HELP_MENU_INSET = 10
CONTROLS_START_X = (BOARD_SIZE.width + 2) * CELL_SIZE.width CONTROLS_START_X = (BOARD_SIZE.width + 2) * CELL_SIZE.width
@staticmethod
@cache
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))
out = ""
for act in am.values():
if out:
out += "\n"
out += f"{pygame.key.name(act.key).rjust(max_key_len)}: {act.desc}"
return out
def __init__(self): def __init__(self):
self._game_over = False self._game_over = False
self.high_score = 0
self._score = 0
@property @property
def game_over(self): def game_over(self) -> bool:
return self._game_over return self._game_over
@game_over.setter @game_over.setter
def game_over(self, newval): def game_over(self, newval: bool):
if newval != self._game_over: if newval != self._game_over:
self.pending_actions = [] self.pending_actions = []
self._game_over = newval self._game_over = newval
@@ -743,6 +800,16 @@ class Game:
else: else:
self.action_map = Game.GAME_ACTION_MAP self.action_map = Game.GAME_ACTION_MAP
@property
def score(self) -> int:
return self._score
@score.setter
def score(self, newval: int):
self._score = newval
if newval > self.high_score:
self.high_score = newval
def random_tetromino(self): def random_tetromino(self):
if not self.cur_random_peice_seq: if not self.cur_random_peice_seq:
self.cur_random_peice_seq = list(Game.TETROMINOS) self.cur_random_peice_seq = list(Game.TETROMINOS)
@@ -999,7 +1066,6 @@ class Game:
elif not any(self.board[next(iter(self.clearing_rows))]): elif 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):
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
@@ -1011,8 +1077,13 @@ class Game:
else: else:
self.clearing_frames += 1 self.clearing_frames += 1
def draw_score(self): def draw_score_and_instructions(self):
text = f"Score: {self.score:05}" text = (
"Press <h> \nfor help.\n"
"\n"
f"Score: {self.score:05}\n"
f" HS: {self.high_score:05}"
)
exts = self.TINY_FONT.compute_extents(text) exts = self.TINY_FONT.compute_extents(text)
x = Game.CONTROLS_START_X + Game.CONTROL_DISPLAY_BORDER x = Game.CONTROLS_START_X + Game.CONTROL_DISPLAY_BORDER
y = ( y = (
@@ -1067,7 +1138,7 @@ class Game:
box_size, box_size,
box_size, box_size,
), ),
width=Game.NEXT_HELD_BOX_SIZE, width=Game.INFO_BOX_BORDER_SIZE,
) )
if piece: if piece:
piece_surf = self.get_scaled_piece(piece, 0.75) piece_surf = self.get_scaled_piece(piece, 0.75)
@@ -1101,11 +1172,44 @@ class Game:
width, width,
) )
def process_and_draw_help_overlay(self):
self.draw_gray_overlay()
hs = Game.make_help_string(Game.GAME_ACTION_MAP)
exts = Game.TINY_FONT.compute_extents(hs)
sw, sh = self.screen.get_size()
tot_border = Game.INFO_BOX_BORDER_SIZE + Game.HELP_MENU_INSET
box_x = sw // 2 - exts.width // 2 - tot_border
box_y = sh // 2 - exts.height // 2 - tot_border
box_w = exts.width + 2 * tot_border
box_h = exts.height + 2 * tot_border
pygame.draw.rect(self.screen, "black", (box_x, box_y, box_w, box_h))
pygame.draw.rect(
self.screen,
Game.PALETTE["gray"].norm,
(box_x, box_y, box_w, box_h),
width=Game.INFO_BOX_BORDER_SIZE,
)
Game.TINY_FONT.center_text(hs, self.screen, sw // 2, sh // 2, "white")
if Action.Type.CLOSE_HELP in self.pending_actions:
self.pending_actions = []
self.action_map = Game.GAME_ACTION_MAP
self.help_mode = False
def game_loop(self): def game_loop(self):
self.doing_drop = False self.doing_drop = False
self.maybe_clear_rows() self.maybe_clear_rows()
if not self.clearing_rows: if Action.Type.OPEN_HELP in self.pending_actions:
# do no matter what
self.help_mode = True
self.action_map = Game.HELP_ACTION_MAP
elif not self.clearing_rows:
self.process_game_actions() self.process_game_actions()
self.advance_piece() self.advance_piece()
else: else:
@@ -1122,7 +1226,10 @@ class Game:
self.draw_board_border() self.draw_board_border()
self.draw_held_and_next_piece() self.draw_held_and_next_piece()
self.draw_score() self.draw_score_and_instructions()
if self.help_mode:
self.process_and_draw_help_overlay()
def start_new_game(self): def start_new_game(self):
self.clear_board() self.clear_board()
@@ -1162,29 +1269,57 @@ class Game:
self.draw_board_border() self.draw_board_border()
self.draw_held_and_next_piece() self.draw_held_and_next_piece()
self.draw_score() self.draw_score_and_instructions()
self.draw_gray_overlay() self.draw_gray_overlay()
ss = self.screen.get_size() sw, sh = self.screen.get_size()
exts = self.FONT.compute_extents("Game Over!") exts = self.FONT.compute_extents("Game Over!")
self.FONT.center_text(
"Game Over!", self.screen, ss[0] // 2, ss[1] // 2, "white"
)
score_text = f"Score: {self.score:05}" score_text = f"Score: {self.score:05}"
self.SMALL_FONT.center_text( exts2 = self.SMALL_FONT.compute_extents(score_text)
score_text, exts3 = self.SMALL_FONT.compute_extents("Press <r> to retry.")
text_width = max(exts.width, exts2.width, exts3.width)
text_height = (
exts.height + exts2.height + exts3.height + 2 * self.FONT.row_kern
)
tot_border = Game.INFO_BOX_BORDER_SIZE + Game.HELP_MENU_INSET
box_x = sw // 2 - text_width // 2 - tot_border
box_y = sh // 2 - text_height // 2 - tot_border
box_w = text_width + 2 * tot_border
box_h = text_height + 2 * tot_border
pygame.draw.rect(self.screen, "black", (box_x, box_y, box_w, box_h))
pygame.draw.rect(
self.screen, self.screen,
ss[0] // 2, Game.PALETTE["gray"].norm,
ss[1] // 2 + exts.height + self.FONT.row_kern, (box_x, box_y, box_w, box_h),
width=Game.INFO_BOX_BORDER_SIZE,
)
self.FONT.render_text(
"Game Over!",
self.screen,
sw // 2 - exts.width // 2,
sh // 2 - text_height // 2,
"white", "white",
) )
exts2 = self.SMALL_FONT.compute_extents(score_text) self.SMALL_FONT.render_text(
self.SMALL_FONT.center_text( score_text,
self.screen,
sw // 2 - exts2.width // 2,
sh // 2 - text_height // 2 + exts.height + self.FONT.row_kern,
"white",
)
self.SMALL_FONT.render_text(
"Press <r> to retry.", "Press <r> to retry.",
self.screen, self.screen,
ss[0] // 2, sw // 2 - exts3.width // 2,
ss[1] // 2 + exts.height + exts2.height + 2 * self.FONT.row_kern, sh // 2
- text_height // 2
+ exts.height
+ exts2.height
+ 2 * self.FONT.row_kern,
"white", "white",
) )
@@ -1232,6 +1367,8 @@ class Game:
self.key_states = defaultdict(lambda: False) self.key_states = defaultdict(lambda: False)
self.pending_actions = [] self.pending_actions = []
self.help_mode = False
self.high_score = 0
self.start_new_game() self.start_new_game()
def run(self): def run(self):