Finish initial gameplay stuff
This commit is contained in:
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user