Finish initial gameplay stuff
This commit is contained in:
@@ -80,6 +80,17 @@ class Tetromino(NamedTuple):
|
||||
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):
|
||||
key: int
|
||||
type: Type
|
||||
@@ -94,13 +105,17 @@ class Action(NamedTuple):
|
||||
SWAP_HOLD = auto()
|
||||
MOVE_LEFT = auto()
|
||||
MOVE_RIGHT = auto()
|
||||
OPEN_HELP = auto()
|
||||
|
||||
# Game over
|
||||
RESTART = auto()
|
||||
|
||||
# Help
|
||||
CLOSE_HELP = auto()
|
||||
|
||||
@staticmethod
|
||||
def make_map(*acts):
|
||||
return {act.key: act for act in acts}
|
||||
return HashableDict({act.key: act for act in acts})
|
||||
|
||||
|
||||
class Font:
|
||||
@@ -151,25 +166,29 @@ class Font:
|
||||
@cache
|
||||
def compute_extents(self, text: str) -> Size:
|
||||
text = text.lower()
|
||||
multi_row = False
|
||||
total_width = 0
|
||||
total_height = 0
|
||||
last_width = 0
|
||||
last_height = 0
|
||||
for char in text:
|
||||
if char == "\n":
|
||||
multi_row = True
|
||||
total_width = max(total_width, last_width)
|
||||
if last_width == 0:
|
||||
# empty line
|
||||
last_height = self.glyphs[" "].height * self.scale
|
||||
last_width = 0
|
||||
total_height += last_height
|
||||
total_height += last_height + self.row_kern
|
||||
last_height = 0
|
||||
else:
|
||||
glyph = self.glyphs[char]
|
||||
last_width += glyph.width * self.scale + self.kern
|
||||
last_height = max(last_height, glyph.height * self.scale)
|
||||
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
|
||||
return Size(total_width, total_height + (multi_row * self.row_kern))
|
||||
return Size(total_width, total_height)
|
||||
|
||||
def render_text(
|
||||
self, text: str, dest: pygame.surface.Surface, x: int, y: int, color
|
||||
@@ -179,6 +198,9 @@ class Font:
|
||||
max_height = 0
|
||||
for char in text:
|
||||
if char == "\n":
|
||||
if x == initial_x:
|
||||
# empty row
|
||||
max_height = self.glyphs[" "].height * self.scale
|
||||
y += max_height + self.row_kern
|
||||
max_height = 0
|
||||
x = initial_x
|
||||
@@ -271,7 +293,7 @@ class Game:
|
||||
pygame.K_s,
|
||||
Action.Type.SWAP_HOLD,
|
||||
False,
|
||||
"Swap the current piece and the held piece.",
|
||||
"Swap the current and held piece.",
|
||||
),
|
||||
Action(
|
||||
pygame.K_DOWN, Action.Type.DROP, True, "Drop the current piece."
|
||||
@@ -288,10 +310,30 @@ class Game:
|
||||
True,
|
||||
"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(
|
||||
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(
|
||||
6,
|
||||
@@ -721,20 +763,35 @@ class Game:
|
||||
],
|
||||
)
|
||||
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
|
||||
NEXT_HELD_BOX_SIZE = 5
|
||||
INFO_BOX_BORDER_SIZE = 5
|
||||
HELP_MENU_INSET = 10
|
||||
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):
|
||||
self._game_over = False
|
||||
self.high_score = 0
|
||||
self._score = 0
|
||||
|
||||
@property
|
||||
def game_over(self):
|
||||
def game_over(self) -> bool:
|
||||
return self._game_over
|
||||
|
||||
@game_over.setter
|
||||
def game_over(self, newval):
|
||||
def game_over(self, newval: bool):
|
||||
if newval != self._game_over:
|
||||
self.pending_actions = []
|
||||
self._game_over = newval
|
||||
@@ -743,6 +800,16 @@ class Game:
|
||||
else:
|
||||
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):
|
||||
if not self.cur_random_peice_seq:
|
||||
self.cur_random_peice_seq = list(Game.TETROMINOS)
|
||||
@@ -999,7 +1066,6 @@ class Game:
|
||||
elif not any(self.board[next(iter(self.clearing_rows))]):
|
||||
# Order matters here
|
||||
for y in sorted(self.clearing_rows):
|
||||
print(y)
|
||||
drop_rows_above_for_clear(y)
|
||||
self.clearing_rows = set()
|
||||
self.clearing_frames = self.CLEAR_SPEED
|
||||
@@ -1011,8 +1077,13 @@ class Game:
|
||||
else:
|
||||
self.clearing_frames += 1
|
||||
|
||||
def draw_score(self):
|
||||
text = f"Score: {self.score:05}"
|
||||
def draw_score_and_instructions(self):
|
||||
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)
|
||||
x = Game.CONTROLS_START_X + Game.CONTROL_DISPLAY_BORDER
|
||||
y = (
|
||||
@@ -1067,7 +1138,7 @@ class Game:
|
||||
box_size,
|
||||
box_size,
|
||||
),
|
||||
width=Game.NEXT_HELD_BOX_SIZE,
|
||||
width=Game.INFO_BOX_BORDER_SIZE,
|
||||
)
|
||||
if piece:
|
||||
piece_surf = self.get_scaled_piece(piece, 0.75)
|
||||
@@ -1101,11 +1172,44 @@ class Game:
|
||||
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):
|
||||
self.doing_drop = False
|
||||
|
||||
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.advance_piece()
|
||||
else:
|
||||
@@ -1122,7 +1226,10 @@ class Game:
|
||||
self.draw_board_border()
|
||||
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):
|
||||
self.clear_board()
|
||||
@@ -1162,29 +1269,57 @@ class Game:
|
||||
self.draw_board_border()
|
||||
self.draw_held_and_next_piece()
|
||||
|
||||
self.draw_score()
|
||||
self.draw_score_and_instructions()
|
||||
|
||||
self.draw_gray_overlay()
|
||||
|
||||
ss = self.screen.get_size()
|
||||
sw, sh = self.screen.get_size()
|
||||
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}"
|
||||
self.SMALL_FONT.center_text(
|
||||
score_text,
|
||||
exts2 = self.SMALL_FONT.compute_extents(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,
|
||||
ss[0] // 2,
|
||||
ss[1] // 2 + exts.height + self.FONT.row_kern,
|
||||
Game.PALETTE["gray"].norm,
|
||||
(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",
|
||||
)
|
||||
exts2 = self.SMALL_FONT.compute_extents(score_text)
|
||||
self.SMALL_FONT.center_text(
|
||||
self.SMALL_FONT.render_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.",
|
||||
self.screen,
|
||||
ss[0] // 2,
|
||||
ss[1] // 2 + exts.height + exts2.height + 2 * self.FONT.row_kern,
|
||||
sw // 2 - exts3.width // 2,
|
||||
sh // 2
|
||||
- text_height // 2
|
||||
+ exts.height
|
||||
+ exts2.height
|
||||
+ 2 * self.FONT.row_kern,
|
||||
"white",
|
||||
)
|
||||
|
||||
@@ -1232,6 +1367,8 @@ class Game:
|
||||
self.key_states = defaultdict(lambda: False)
|
||||
self.pending_actions = []
|
||||
|
||||
self.help_mode = False
|
||||
self.high_score = 0
|
||||
self.start_new_game()
|
||||
|
||||
def run(self):
|
||||
|
||||
Reference in New Issue
Block a user