Add text, score, and game over
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
import pygame
|
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
|
from typing import NamedTuple, Any, Iterable, Optional
|
||||||
|
from functools import cache
|
||||||
import random
|
import random
|
||||||
import math
|
import math
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
@@ -80,6 +81,7 @@ class Action(NamedTuple):
|
|||||||
desc: str
|
desc: str
|
||||||
|
|
||||||
class Type(Enum):
|
class Type(Enum):
|
||||||
|
# In Game
|
||||||
ROTATE_LEFT = auto()
|
ROTATE_LEFT = auto()
|
||||||
ROTATE_RIGHT = auto()
|
ROTATE_RIGHT = auto()
|
||||||
DROP = auto()
|
DROP = auto()
|
||||||
@@ -87,15 +89,136 @@ class Action(NamedTuple):
|
|||||||
MOVE_LEFT = auto()
|
MOVE_LEFT = auto()
|
||||||
MOVE_RIGHT = auto()
|
MOVE_RIGHT = auto()
|
||||||
|
|
||||||
|
# Game over
|
||||||
|
RESTART = auto()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def make_map(*acts):
|
def make_map(*acts):
|
||||||
return {act.key: act for act in acts}
|
return {act.key: act for act in acts}
|
||||||
|
|
||||||
|
|
||||||
|
class Font:
|
||||||
|
class Glyph(NamedTuple):
|
||||||
|
char: str
|
||||||
|
bitmap: list[str]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def width(self):
|
||||||
|
return len(self.bitmap[0])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def height(self):
|
||||||
|
return len(self.bitmap)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def size(self) -> Size:
|
||||||
|
return Size(self.width, self.height)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
scale: int,
|
||||||
|
kern: int,
|
||||||
|
row_kern: int,
|
||||||
|
glyphs: Iterable[Glyph] | dict[str, Glyph],
|
||||||
|
):
|
||||||
|
self.scale = scale
|
||||||
|
self.kern = kern
|
||||||
|
self.row_kern = row_kern
|
||||||
|
if isinstance(glyphs, dict):
|
||||||
|
self.glyphs: dict[str, Font.Glyph] = dict(glyphs)
|
||||||
|
else:
|
||||||
|
self.glyphs: dict[str, Font.Glyph] = {g.char: g for g in glyphs}
|
||||||
|
|
||||||
|
def scaled(
|
||||||
|
self,
|
||||||
|
new_scale: int,
|
||||||
|
new_kern: Optional[int],
|
||||||
|
new_row_kern: Optional[int],
|
||||||
|
):
|
||||||
|
return Font(
|
||||||
|
new_scale,
|
||||||
|
self.kern if new_kern is None else new_kern,
|
||||||
|
self.row_kern if new_row_kern is None else new_row_kern,
|
||||||
|
self.glyphs,
|
||||||
|
)
|
||||||
|
|
||||||
|
@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)
|
||||||
|
last_width = 0
|
||||||
|
total_height += last_height
|
||||||
|
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
|
||||||
|
total_height += last_height
|
||||||
|
return Size(total_width, total_height + (multi_row * self.row_kern))
|
||||||
|
|
||||||
|
def render_text(
|
||||||
|
self, text: str, dest: pygame.surface.Surface, x: int, y: int, color
|
||||||
|
):
|
||||||
|
text = text.lower()
|
||||||
|
initial_x = x
|
||||||
|
max_height = 0
|
||||||
|
for char in text:
|
||||||
|
if char == "\n":
|
||||||
|
y += max_height + self.row_kern
|
||||||
|
max_height = 0
|
||||||
|
x = initial_x
|
||||||
|
else:
|
||||||
|
glyph = self.glyphs[char]
|
||||||
|
dest.blit(self.render_glyph(char, color), (x, y))
|
||||||
|
x += glyph.width * self.scale + self.kern
|
||||||
|
max_height = max(max_height, glyph.height * self.scale)
|
||||||
|
|
||||||
|
def center_text(
|
||||||
|
self, text: str, dest: pygame.surface.Surface, x: int, y: int, color
|
||||||
|
):
|
||||||
|
text = text.lower()
|
||||||
|
exts = self.compute_extents(text)
|
||||||
|
self.render_text(
|
||||||
|
text, dest, x - (exts.width // 2), y - (exts.height // 2), color
|
||||||
|
)
|
||||||
|
|
||||||
|
@cache
|
||||||
|
def render_glyph(self, char: str, color) -> pygame.surface.Surface:
|
||||||
|
char = char.lower()
|
||||||
|
glyph = self.glyphs[char]
|
||||||
|
size = tuple(map(lambda d: d * self.scale, glyph.size))
|
||||||
|
surf = pygame.surface.Surface(size, pygame.SRCALPHA)
|
||||||
|
for y, row in enumerate(glyph.bitmap):
|
||||||
|
for x, cell in enumerate(row):
|
||||||
|
if cell != " ":
|
||||||
|
pygame.draw.rect(
|
||||||
|
surf,
|
||||||
|
color,
|
||||||
|
(
|
||||||
|
x * self.scale,
|
||||||
|
y * self.scale,
|
||||||
|
self.scale,
|
||||||
|
self.scale,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return surf
|
||||||
|
|
||||||
|
|
||||||
class Game:
|
class Game:
|
||||||
|
PLACE_POINTS = 10
|
||||||
|
CLEAR_POINTS = 100
|
||||||
MOVE_SPEED = 20 # cells per second
|
MOVE_SPEED = 20 # cells per second
|
||||||
NORMAL_DROP_SPEED = 3 # cells per second
|
NORMAL_DROP_SPEED = 3 # cells per second
|
||||||
FAST_DROP_SPEED = 10 # cells per second
|
FAST_DROP_SPEED = 30 # cells per second
|
||||||
CLEAR_SPEED = 5 # frames per block
|
CLEAR_SPEED = 5 # 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)
|
||||||
@@ -125,8 +248,7 @@ class Game:
|
|||||||
Tetromino("green", [[0, 1, 1], [1, 1, 0]]),
|
Tetromino("green", [[0, 1, 1], [1, 1, 0]]),
|
||||||
Tetromino("red", [[1, 1, 0], [0, 1, 1]]),
|
Tetromino("red", [[1, 1, 0], [0, 1, 1]]),
|
||||||
]
|
]
|
||||||
|
GAME_ACTION_MAP = Action.make_map(
|
||||||
ACTION_MAP = Action.make_map(
|
|
||||||
Action(
|
Action(
|
||||||
pygame.K_z,
|
pygame.K_z,
|
||||||
Action.Type.ROTATE_LEFT,
|
Action.Type.ROTATE_LEFT,
|
||||||
@@ -161,15 +283,462 @@ class Game:
|
|||||||
"Move the current piece right.",
|
"Move the current piece right.",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
GAME_OVER_ACTION_MAP = Action.make_map(
|
||||||
|
Action(pygame.K_r, Action.Type.RESTART, False, "Start a new game.")
|
||||||
|
)
|
||||||
|
|
||||||
|
FONT = Font(
|
||||||
|
6,
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
[
|
||||||
|
Font.Glyph(
|
||||||
|
" ",
|
||||||
|
[
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"a",
|
||||||
|
[
|
||||||
|
"*****",
|
||||||
|
"* *",
|
||||||
|
"*****",
|
||||||
|
"* *",
|
||||||
|
"* *",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"b",
|
||||||
|
[
|
||||||
|
"**** ",
|
||||||
|
"* *",
|
||||||
|
"*****",
|
||||||
|
"* *",
|
||||||
|
"**** ",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"c",
|
||||||
|
[
|
||||||
|
"*****",
|
||||||
|
"* *",
|
||||||
|
"* ",
|
||||||
|
"* *",
|
||||||
|
"*****",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"d",
|
||||||
|
[
|
||||||
|
"**** ",
|
||||||
|
"* *",
|
||||||
|
"* *",
|
||||||
|
"* *",
|
||||||
|
"**** ",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"e",
|
||||||
|
[
|
||||||
|
"*****",
|
||||||
|
"* ",
|
||||||
|
"**** ",
|
||||||
|
"* ",
|
||||||
|
"*****",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"f",
|
||||||
|
[
|
||||||
|
"*****",
|
||||||
|
"* ",
|
||||||
|
"**** ",
|
||||||
|
"* ",
|
||||||
|
"* ",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"g",
|
||||||
|
[
|
||||||
|
"*****",
|
||||||
|
"* ",
|
||||||
|
"* **",
|
||||||
|
"* *",
|
||||||
|
"*****",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"h",
|
||||||
|
[
|
||||||
|
"* *",
|
||||||
|
"* *",
|
||||||
|
"*****",
|
||||||
|
"* *",
|
||||||
|
"* *",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"i",
|
||||||
|
[
|
||||||
|
"*****",
|
||||||
|
" * ",
|
||||||
|
" * ",
|
||||||
|
" * ",
|
||||||
|
"*****",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"j",
|
||||||
|
[
|
||||||
|
"*****",
|
||||||
|
" * ",
|
||||||
|
" * ",
|
||||||
|
"* * ",
|
||||||
|
" ** ",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"k",
|
||||||
|
[
|
||||||
|
"* *",
|
||||||
|
"* * ",
|
||||||
|
"*** ",
|
||||||
|
"* * ",
|
||||||
|
"* *",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"l",
|
||||||
|
[
|
||||||
|
"* ",
|
||||||
|
"* ",
|
||||||
|
"* ",
|
||||||
|
"* ",
|
||||||
|
"*****",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"m",
|
||||||
|
[
|
||||||
|
"* *",
|
||||||
|
"** **",
|
||||||
|
"* * *",
|
||||||
|
"* *",
|
||||||
|
"* *",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"n",
|
||||||
|
[
|
||||||
|
"* *",
|
||||||
|
"** *",
|
||||||
|
"* * *",
|
||||||
|
"* **",
|
||||||
|
"* *",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"o",
|
||||||
|
[
|
||||||
|
" *** ",
|
||||||
|
"* *",
|
||||||
|
"* *",
|
||||||
|
"* *",
|
||||||
|
" *** ",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"p",
|
||||||
|
[
|
||||||
|
"**** ",
|
||||||
|
"* *",
|
||||||
|
"**** ",
|
||||||
|
"* ",
|
||||||
|
"* ",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"q",
|
||||||
|
[
|
||||||
|
" *** ",
|
||||||
|
"* *",
|
||||||
|
"* * *",
|
||||||
|
"* * ",
|
||||||
|
" ** *",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"r",
|
||||||
|
[
|
||||||
|
"**** ",
|
||||||
|
"* *",
|
||||||
|
"**** ",
|
||||||
|
"* * ",
|
||||||
|
"* *",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"s",
|
||||||
|
[
|
||||||
|
" ****",
|
||||||
|
"* ",
|
||||||
|
" *** ",
|
||||||
|
" *",
|
||||||
|
"**** ",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"t",
|
||||||
|
[
|
||||||
|
"*****",
|
||||||
|
" * ",
|
||||||
|
" * ",
|
||||||
|
" * ",
|
||||||
|
" * ",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"u",
|
||||||
|
[
|
||||||
|
"* *",
|
||||||
|
"* *",
|
||||||
|
"* *",
|
||||||
|
"* *",
|
||||||
|
" *** ",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"v",
|
||||||
|
[
|
||||||
|
"* *",
|
||||||
|
"* *",
|
||||||
|
"* *",
|
||||||
|
" * * ",
|
||||||
|
" * ",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"w",
|
||||||
|
[
|
||||||
|
"* *",
|
||||||
|
"* *",
|
||||||
|
"* * *",
|
||||||
|
"** **",
|
||||||
|
"* *",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"x",
|
||||||
|
[
|
||||||
|
"* *",
|
||||||
|
" * * ",
|
||||||
|
" * ",
|
||||||
|
" * * ",
|
||||||
|
"* *",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"y",
|
||||||
|
[
|
||||||
|
"* *",
|
||||||
|
" * * ",
|
||||||
|
" * ",
|
||||||
|
" * ",
|
||||||
|
" * ",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"z",
|
||||||
|
[
|
||||||
|
"*****",
|
||||||
|
" * ",
|
||||||
|
" * ",
|
||||||
|
" * ",
|
||||||
|
"*****",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"0",
|
||||||
|
[
|
||||||
|
" *** ",
|
||||||
|
"* *",
|
||||||
|
"* * *",
|
||||||
|
"* *",
|
||||||
|
" *** ",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"1",
|
||||||
|
[
|
||||||
|
" ** ",
|
||||||
|
"* * ",
|
||||||
|
" * ",
|
||||||
|
" * ",
|
||||||
|
"*****",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"2",
|
||||||
|
[
|
||||||
|
" *** ",
|
||||||
|
"* *",
|
||||||
|
" * ",
|
||||||
|
" * ",
|
||||||
|
" ****",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"3",
|
||||||
|
[
|
||||||
|
" *** ",
|
||||||
|
"* *",
|
||||||
|
" ***",
|
||||||
|
"* *",
|
||||||
|
" *** ",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"4",
|
||||||
|
[
|
||||||
|
"* * ",
|
||||||
|
"* * ",
|
||||||
|
"*****",
|
||||||
|
" * ",
|
||||||
|
" * ",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"5",
|
||||||
|
[
|
||||||
|
"*****",
|
||||||
|
"* ",
|
||||||
|
"**** ",
|
||||||
|
" *",
|
||||||
|
"**** ",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"6",
|
||||||
|
[
|
||||||
|
" ****",
|
||||||
|
"* ",
|
||||||
|
"**** ",
|
||||||
|
"* *",
|
||||||
|
" *** ",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"7",
|
||||||
|
[
|
||||||
|
"*****",
|
||||||
|
" * ",
|
||||||
|
" * ",
|
||||||
|
" * ",
|
||||||
|
"* ",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"8",
|
||||||
|
[
|
||||||
|
" *** ",
|
||||||
|
"* *",
|
||||||
|
" *** ",
|
||||||
|
"* *",
|
||||||
|
" *** ",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"9",
|
||||||
|
[
|
||||||
|
" *** ",
|
||||||
|
"* *",
|
||||||
|
" ****",
|
||||||
|
" *",
|
||||||
|
"**** ",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
".",
|
||||||
|
[
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
" *",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"!",
|
||||||
|
[
|
||||||
|
" *",
|
||||||
|
" *",
|
||||||
|
" *",
|
||||||
|
" ",
|
||||||
|
" *",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
":",
|
||||||
|
[
|
||||||
|
" ",
|
||||||
|
" *",
|
||||||
|
" ",
|
||||||
|
" *",
|
||||||
|
" ",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
"<",
|
||||||
|
[
|
||||||
|
" **",
|
||||||
|
" ** ",
|
||||||
|
"* ",
|
||||||
|
" ** ",
|
||||||
|
" **",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Font.Glyph(
|
||||||
|
">",
|
||||||
|
[
|
||||||
|
"** ",
|
||||||
|
" ** ",
|
||||||
|
" *",
|
||||||
|
" ** ",
|
||||||
|
"** ",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
SMALL_FONT = FONT.scaled(4, 2, 2)
|
||||||
|
TINY_FONT = FONT.scaled(2, 1, 1)
|
||||||
|
SCORE_DISPLAY_BORDER = 5
|
||||||
|
CONTROLS_START_X = (BOARD_SIZE.width + 2) * CELL_SIZE.width
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def random_tetromino():
|
def random_tetromino():
|
||||||
return random.choice(Game.TETROMINOS)
|
return random.choice(Game.TETROMINOS)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.screen = None
|
self._game_over = False
|
||||||
self.clock = None
|
|
||||||
self.running = False
|
@property
|
||||||
|
def game_over(self):
|
||||||
|
return self._game_over
|
||||||
|
|
||||||
|
@game_over.setter
|
||||||
|
def game_over(self, newval):
|
||||||
|
if newval != self._game_over:
|
||||||
|
self.pending_actions = []
|
||||||
|
self._game_over = newval
|
||||||
|
if self._game_over:
|
||||||
|
self.action_map = Game.GAME_OVER_ACTION_MAP
|
||||||
|
else:
|
||||||
|
self.action_map = Game.GAME_ACTION_MAP
|
||||||
|
|
||||||
def draw_block(self, x: int, y: int, color):
|
def draw_block(self, x: int, y: int, color):
|
||||||
"""Draw a block at (X, Y). Coordinates are for the top left corner."""
|
"""Draw a block at (X, Y). Coordinates are for the top left corner."""
|
||||||
@@ -259,12 +828,17 @@ class Game:
|
|||||||
screen_x, screen_y, self.current_block.color
|
screen_x, screen_y, self.current_block.color
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def frame_time(self) -> float:
|
||||||
|
ticks = self.clock.get_time()
|
||||||
|
if not ticks:
|
||||||
|
return 1 / Game.FRAMERATE
|
||||||
|
else:
|
||||||
|
return ticks / 1000
|
||||||
|
|
||||||
def handle_key_event(self, typ, key):
|
def handle_key_event(self, typ, key):
|
||||||
if key not in Game.ACTION_MAP:
|
if typ == pygame.KEYDOWN:
|
||||||
return
|
if key in self.action_map:
|
||||||
elif typ == pygame.KEYDOWN:
|
self.pending_actions.append(self.action_map[key].type)
|
||||||
act = Game.ACTION_MAP[key]
|
|
||||||
self.pending_actions.append(act.type)
|
|
||||||
self.key_states[key] = True
|
self.key_states[key] = True
|
||||||
elif typ == pygame.KEYUP:
|
elif typ == pygame.KEYUP:
|
||||||
self.key_states[key] = False
|
self.key_states[key] = False
|
||||||
@@ -274,10 +848,10 @@ class Game:
|
|||||||
for key, state in self.key_states.items():
|
for key, state in self.key_states.items():
|
||||||
if (
|
if (
|
||||||
state
|
state
|
||||||
and key in self.ACTION_MAP
|
and key in self.action_map
|
||||||
and self.ACTION_MAP[key].repeat
|
and self.action_map[key].repeat
|
||||||
):
|
):
|
||||||
self.pending_actions.append(self.ACTION_MAP[key].type)
|
self.pending_actions.append(self.action_map[key].type)
|
||||||
# now process new events
|
# now process new events
|
||||||
for event in pygame.event.get():
|
for event in pygame.event.get():
|
||||||
match event.type:
|
match event.type:
|
||||||
@@ -287,7 +861,10 @@ class Game:
|
|||||||
self.handle_key_event(event.type, event.key)
|
self.handle_key_event(event.type, event.key)
|
||||||
|
|
||||||
def swap_with_hold(self):
|
def swap_with_hold(self):
|
||||||
pass
|
to_swap = self.held_piece or Game.random_tetromino()
|
||||||
|
if not self.intersects_board(*self.current_block_pos, to_swap):
|
||||||
|
self.held_piece = self.current_block
|
||||||
|
self.current_block = to_swap
|
||||||
|
|
||||||
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.width, piece.height
|
||||||
@@ -314,7 +891,7 @@ class Game:
|
|||||||
return
|
return
|
||||||
|
|
||||||
def move_current_piece(self, dir: Direction):
|
def move_current_piece(self, dir: Direction):
|
||||||
dx = dir * Game.MOVE_SPEED / Game.FRAMERATE
|
dx = dir * Game.MOVE_SPEED * self.frame_time()
|
||||||
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
|
||||||
@@ -325,7 +902,7 @@ class Game:
|
|||||||
if not self.intersects_board(*new_pos, self.current_block):
|
if not self.intersects_board(*new_pos, self.current_block):
|
||||||
self.current_block_pos = new_pos
|
self.current_block_pos = new_pos
|
||||||
|
|
||||||
def process_actions(self):
|
def process_game_actions(self):
|
||||||
if not (
|
if not (
|
||||||
{Action.Type.MOVE_LEFT, Action.Type.MOVE_RIGHT}
|
{Action.Type.MOVE_LEFT, Action.Type.MOVE_RIGHT}
|
||||||
& set(self.pending_actions)
|
& set(self.pending_actions)
|
||||||
@@ -347,6 +924,7 @@ class Game:
|
|||||||
self.move_current_piece(Direction.RIGHT)
|
self.move_current_piece(Direction.RIGHT)
|
||||||
|
|
||||||
def place_piece(self, x: int, y: int, piece: Tetromino):
|
def place_piece(self, x: int, y: int, piece: Tetromino):
|
||||||
|
self.score += Game.PLACE_POINTS
|
||||||
for dy, row in enumerate(piece.shape):
|
for dy, row in enumerate(piece.shape):
|
||||||
for dx, cell in enumerate(row):
|
for dx, cell in enumerate(row):
|
||||||
if cell:
|
if cell:
|
||||||
@@ -356,7 +934,7 @@ class Game:
|
|||||||
speed = (
|
speed = (
|
||||||
Game.FAST_DROP_SPEED if self.doing_drop else Game.NORMAL_DROP_SPEED
|
Game.FAST_DROP_SPEED if self.doing_drop else Game.NORMAL_DROP_SPEED
|
||||||
)
|
)
|
||||||
self.subcell_drop += speed / Game.FRAMERATE
|
self.subcell_drop += speed * self.frame_time()
|
||||||
move_cell = int(self.subcell_drop)
|
move_cell = int(self.subcell_drop)
|
||||||
self.subcell_drop -= move_cell
|
self.subcell_drop -= move_cell
|
||||||
cp = self.current_block_pos
|
cp = self.current_block_pos
|
||||||
@@ -372,8 +950,7 @@ class Game:
|
|||||||
self.place_piece(cp.x, cp.y + dy, self.current_block)
|
self.place_piece(cp.x, cp.y + dy, self.current_block)
|
||||||
self.current_block = None
|
self.current_block = None
|
||||||
return
|
return
|
||||||
# TODO Game over
|
self.game_over = True
|
||||||
print("Game over!")
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generate_clear_cells():
|
def generate_clear_cells():
|
||||||
@@ -399,6 +976,7 @@ class Game:
|
|||||||
if not self.clearing_rows:
|
if not self.clearing_rows:
|
||||||
for y, row in enumerate(self.board):
|
for y, row in enumerate(self.board):
|
||||||
if all(row):
|
if all(row):
|
||||||
|
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))]):
|
elif not any(self.board[next(iter(self.clearing_rows))]):
|
||||||
for y in self.clearing_rows:
|
for y in self.clearing_rows:
|
||||||
@@ -413,16 +991,19 @@ class Game:
|
|||||||
else:
|
else:
|
||||||
self.clearing_frames += 1
|
self.clearing_frames += 1
|
||||||
|
|
||||||
def loop(self):
|
def draw_score(self):
|
||||||
self.running = True
|
text = f"Score: {self.score:05}"
|
||||||
while self.running:
|
exts = self.TINY_FONT.compute_extents(text)
|
||||||
self.doing_drop = False
|
x = Game.CONTROLS_START_X + Game.SCORE_DISPLAY_BORDER
|
||||||
|
y = self.screen.get_size()[1] - Game.SCORE_DISPLAY_BORDER - exts.height
|
||||||
|
self.TINY_FONT.render_text(text, self.screen, x, y, "white")
|
||||||
|
|
||||||
self.handle_events()
|
def game_loop(self):
|
||||||
|
self.doing_drop = False
|
||||||
|
|
||||||
self.maybe_clear_rows()
|
self.maybe_clear_rows()
|
||||||
if not self.clearing_rows:
|
if not self.clearing_rows:
|
||||||
self.process_actions()
|
self.process_game_actions()
|
||||||
self.advance_piece()
|
self.advance_piece()
|
||||||
else:
|
else:
|
||||||
# ignore input while clearing
|
# ignore input while clearing
|
||||||
@@ -437,6 +1018,79 @@ class Game:
|
|||||||
self.draw_board_content()
|
self.draw_board_content()
|
||||||
self.draw_board_border()
|
self.draw_board_border()
|
||||||
|
|
||||||
|
self.draw_score()
|
||||||
|
|
||||||
|
def start_new_game(self):
|
||||||
|
self.clear_board()
|
||||||
|
self.generate_new_block()
|
||||||
|
self.held_piece = None
|
||||||
|
self.subcell_move = 0
|
||||||
|
self.subcell_drop = 0
|
||||||
|
self.clearing_rows = set()
|
||||||
|
# immediately clear the first block
|
||||||
|
self.clearing_frames = Game.CLEAR_SPEED
|
||||||
|
self.score = 0
|
||||||
|
self.held_piece = None
|
||||||
|
self.game_over = False
|
||||||
|
|
||||||
|
def handle_game_over_actions(self):
|
||||||
|
while self.pending_actions:
|
||||||
|
act = self.pending_actions.pop(0)
|
||||||
|
if act == Action.Type.RESTART:
|
||||||
|
self.start_new_game()
|
||||||
|
|
||||||
|
def make_gray_overlay(self):
|
||||||
|
s = pygame.surface.Surface(self.screen.get_size(), pygame.SRCALPHA)
|
||||||
|
pygame.draw.rect(s, "#44444466", (0, 0, *s.get_size()))
|
||||||
|
return s
|
||||||
|
|
||||||
|
def draw_gray_overlay(self):
|
||||||
|
self.screen.blit(self.gray_overlay, (0, 0))
|
||||||
|
|
||||||
|
def game_over_loop(self):
|
||||||
|
self.handle_game_over_actions()
|
||||||
|
if not self.game_over:
|
||||||
|
return
|
||||||
|
self.screen.fill("black")
|
||||||
|
self.draw_board_content()
|
||||||
|
self.draw_current_block()
|
||||||
|
self.draw_board_border()
|
||||||
|
|
||||||
|
self.draw_score()
|
||||||
|
|
||||||
|
self.draw_gray_overlay()
|
||||||
|
|
||||||
|
ss = 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,
|
||||||
|
self.screen,
|
||||||
|
ss[0] // 2,
|
||||||
|
ss[1] // 2 + exts.height + self.FONT.row_kern,
|
||||||
|
"white",
|
||||||
|
)
|
||||||
|
exts2 = self.SMALL_FONT.compute_extents(score_text)
|
||||||
|
self.SMALL_FONT.center_text(
|
||||||
|
"Press <r> to retry.",
|
||||||
|
self.screen,
|
||||||
|
ss[0] // 2,
|
||||||
|
ss[1] // 2 + exts.height + exts2.height + 2 * self.FONT.row_kern,
|
||||||
|
"white",
|
||||||
|
)
|
||||||
|
|
||||||
|
def loop(self):
|
||||||
|
self.running = True
|
||||||
|
while self.running:
|
||||||
|
self.handle_events()
|
||||||
|
if self.game_over:
|
||||||
|
self.game_over_loop()
|
||||||
|
else:
|
||||||
|
self.game_loop()
|
||||||
|
|
||||||
pygame.display.flip()
|
pygame.display.flip()
|
||||||
self.clock.tick(Game.FRAMERATE)
|
self.clock.tick(Game.FRAMERATE)
|
||||||
|
|
||||||
@@ -449,25 +1103,22 @@ class Game:
|
|||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def clear_board(self):
|
||||||
|
self.board = make_matrix(*Game.BOARD_SIZE)
|
||||||
|
|
||||||
def init(self):
|
def init(self):
|
||||||
pygame.init()
|
pygame.init()
|
||||||
self.screen = pygame.display.set_mode(Game.WINDOW_SIZE, pygame.SCALED)
|
self.screen = pygame.display.set_mode(Game.WINDOW_SIZE, pygame.SCALED)
|
||||||
pygame.display.set_caption("Tetris")
|
pygame.display.set_caption("Tetris")
|
||||||
self.clock = pygame.time.Clock()
|
self.clock = pygame.time.Clock()
|
||||||
|
|
||||||
|
self.gray_overlay = self.make_gray_overlay()
|
||||||
|
|
||||||
pygame.key.stop_text_input()
|
pygame.key.stop_text_input()
|
||||||
self.key_states = defaultdict(lambda: False)
|
self.key_states = defaultdict(lambda: False)
|
||||||
self.pending_actions = []
|
self.pending_actions = []
|
||||||
|
|
||||||
# Game state
|
self.start_new_game()
|
||||||
self.board = make_matrix(*Game.BOARD_SIZE)
|
|
||||||
self.generate_new_block()
|
|
||||||
self.held_piece = None
|
|
||||||
self.subcell_move = 0
|
|
||||||
self.subcell_drop = 0
|
|
||||||
self.clearing_rows = set()
|
|
||||||
# immediately clear the first block
|
|
||||||
self.clearing_frames = Game.CLEAR_SPEED
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self.init()
|
self.init()
|
||||||
|
|||||||
Reference in New Issue
Block a user