Work on game
This commit is contained in:
@@ -1,15 +1,27 @@
|
|||||||
import pygame
|
import pygame
|
||||||
import pygame.draw as draw
|
import pygame.draw as draw
|
||||||
from collections import namedtuple
|
from collections import namedtuple, defaultdict
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple, Any
|
||||||
import random
|
import random
|
||||||
import math
|
import math
|
||||||
|
from enum import Enum, auto
|
||||||
|
|
||||||
Cell = namedtuple("Cell", ["x", "y"])
|
Cell = namedtuple("Cell", ["x", "y"])
|
||||||
Size = namedtuple("Size", ["width", "height"])
|
Size = namedtuple("Size", ["width", "height"])
|
||||||
ColorSet = namedtuple("ColorSet", ["norm", "light", "dark"])
|
ColorSet = namedtuple("ColorSet", ["norm", "light", "dark"])
|
||||||
|
|
||||||
|
|
||||||
|
class Direction(int, Enum):
|
||||||
|
LEFT = -1
|
||||||
|
RIGHT = 1
|
||||||
|
|
||||||
|
|
||||||
|
def make_matrix(w: int, h: int, elt: Any = None) -> list[list]:
|
||||||
|
if not w:
|
||||||
|
return []
|
||||||
|
return [[elt] * w for _ in range(h)]
|
||||||
|
|
||||||
|
|
||||||
def flip(mat: list[list]) -> list[list]:
|
def flip(mat: list[list]) -> list[list]:
|
||||||
return list(reversed(mat))
|
return list(reversed(mat))
|
||||||
|
|
||||||
@@ -17,7 +29,7 @@ def flip(mat: list[list]) -> list[list]:
|
|||||||
def transpose(mat: list[list]) -> list[list]:
|
def transpose(mat: list[list]) -> list[list]:
|
||||||
if not mat:
|
if not mat:
|
||||||
return []
|
return []
|
||||||
out = [list([None] * len(mat)) for _ in range(len(mat[0]))]
|
out = make_matrix(len(mat), len(mat[0]))
|
||||||
for x in range(len(mat[0])):
|
for x in range(len(mat[0])):
|
||||||
for y in range(len(mat)):
|
for y in range(len(mat)):
|
||||||
out[x][y] = mat[y][x]
|
out[x][y] = mat[y][x]
|
||||||
@@ -53,10 +65,38 @@ class Tetromino(NamedTuple):
|
|||||||
return Tetromino(self.color, flip(transpose(self.shape)))
|
return Tetromino(self.color, flip(transpose(self.shape)))
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "\n".join(["".join(row) for row in self.shape])
|
out = []
|
||||||
|
for row in self.shape:
|
||||||
|
for cell in row:
|
||||||
|
out.append("*" if cell else " ")
|
||||||
|
out.append("\n")
|
||||||
|
return "".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
class Action(NamedTuple):
|
||||||
|
key: int
|
||||||
|
type: Type
|
||||||
|
repeat: bool
|
||||||
|
desc: str
|
||||||
|
|
||||||
|
class Type(Enum):
|
||||||
|
ROTATE_LEFT = auto()
|
||||||
|
ROTATE_RIGHT = auto()
|
||||||
|
DROP = auto()
|
||||||
|
SWAP_HOLD = auto()
|
||||||
|
MOVE_LEFT = auto()
|
||||||
|
MOVE_RIGHT = auto()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def make_map(*acts):
|
||||||
|
return {act.key: act for act in acts}
|
||||||
|
|
||||||
|
|
||||||
class Game:
|
class Game:
|
||||||
|
MOVE_SPEED = 20 # cells per second
|
||||||
|
NORMAL_DROP_SPEED = 3 # cells per second
|
||||||
|
FAST_DROP_SPEED = 10 # cells per second
|
||||||
|
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)
|
||||||
CELL_BORDER_SIZE = CELL_SIZE.width // 10
|
CELL_BORDER_SIZE = CELL_SIZE.width // 10
|
||||||
@@ -77,15 +117,51 @@ class Game:
|
|||||||
"green": ColorSet("#00cd00", "#00ff00", "#009a00"),
|
"green": ColorSet("#00cd00", "#00ff00", "#009a00"),
|
||||||
}
|
}
|
||||||
TETROMINOS = [
|
TETROMINOS = [
|
||||||
Tetromino("aqua", [["*", "*", "*", "*"]]),
|
Tetromino("aqua", [[1, 1, 1, 1]]),
|
||||||
Tetromino("yellow", [["*", "*"], ["*", "*"]]),
|
Tetromino("yellow", [[1, 1], [1, 1]]),
|
||||||
Tetromino("purple", [["*", "*", "*"], [" ", "*", " "]]),
|
Tetromino("purple", [[1, 1, 1], [0, 1, 0]]),
|
||||||
Tetromino("blue", [[" ", "*"], [" ", "*"], ["*", "*"]]),
|
Tetromino("blue", [[0, 1], [0, 1], [1, 1]]),
|
||||||
Tetromino("orange", [["*", " "], ["*", " "], ["*", "*"]]),
|
Tetromino("orange", [[1, 0], [1, 0], [1, 1]]),
|
||||||
Tetromino("green", [[" ", "*", "*"], ["*", "*", " "]]),
|
Tetromino("green", [[0, 1, 1], [1, 1, 0]]),
|
||||||
Tetromino("red", [["*", "*", " "], [" ", "*", "*"]]),
|
Tetromino("red", [[1, 1, 0], [0, 1, 1]]),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
ACTION_MAP = Action.make_map(
|
||||||
|
Action(
|
||||||
|
pygame.K_z,
|
||||||
|
Action.Type.ROTATE_LEFT,
|
||||||
|
False,
|
||||||
|
"Rotate the current piece left.",
|
||||||
|
),
|
||||||
|
Action(
|
||||||
|
pygame.K_x,
|
||||||
|
Action.Type.ROTATE_RIGHT,
|
||||||
|
False,
|
||||||
|
"Rotate the current piece right.",
|
||||||
|
),
|
||||||
|
Action(
|
||||||
|
pygame.K_s,
|
||||||
|
Action.Type.SWAP_HOLD,
|
||||||
|
False,
|
||||||
|
"Swap the current piece and the held piece.",
|
||||||
|
),
|
||||||
|
Action(
|
||||||
|
pygame.K_DOWN, Action.Type.DROP, True, "Drop the current piece."
|
||||||
|
),
|
||||||
|
Action(
|
||||||
|
pygame.K_LEFT,
|
||||||
|
Action.Type.MOVE_LEFT,
|
||||||
|
True,
|
||||||
|
"Move the current piece left.",
|
||||||
|
),
|
||||||
|
Action(
|
||||||
|
pygame.K_RIGHT,
|
||||||
|
Action.Type.MOVE_RIGHT,
|
||||||
|
True,
|
||||||
|
"Move the current piece right.",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def random_tetromino():
|
def random_tetromino():
|
||||||
return random.choice(Game.TETROMINOS)
|
return random.choice(Game.TETROMINOS)
|
||||||
@@ -176,26 +252,191 @@ class Game:
|
|||||||
start_x, start_y = self.current_block_pos
|
start_x, start_y = self.current_block_pos
|
||||||
for x in range(self.current_block.width):
|
for x in range(self.current_block.width):
|
||||||
for y in range(self.current_block.height):
|
for y in range(self.current_block.height):
|
||||||
if self.current_block.shape[y][x] == "*":
|
if self.current_block.shape[y][x]:
|
||||||
screen_x = (start_x + x + 1) * Game.CELL_SIZE.width
|
screen_x = (start_x + x + 1) * Game.CELL_SIZE.width
|
||||||
screen_y = (start_y + y + 1) * Game.CELL_SIZE.height
|
screen_y = (start_y + y + 1) * Game.CELL_SIZE.height
|
||||||
self.draw_block(
|
self.draw_block(
|
||||||
screen_x, screen_y, self.current_block.color
|
screen_x, screen_y, self.current_block.color
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def handle_key_event(self, typ, key):
|
||||||
|
if key not in Game.ACTION_MAP:
|
||||||
|
return
|
||||||
|
elif typ == pygame.KEYDOWN:
|
||||||
|
act = Game.ACTION_MAP[key]
|
||||||
|
self.pending_actions.append(act.type)
|
||||||
|
self.key_states[key] = True
|
||||||
|
elif typ == pygame.KEYUP:
|
||||||
|
self.key_states[key] = False
|
||||||
|
|
||||||
def handle_events(self):
|
def handle_events(self):
|
||||||
|
# process already pushed keys
|
||||||
|
for key, state in self.key_states.items():
|
||||||
|
if (
|
||||||
|
state
|
||||||
|
and key in self.ACTION_MAP
|
||||||
|
and self.ACTION_MAP[key].repeat
|
||||||
|
):
|
||||||
|
self.pending_actions.append(self.ACTION_MAP[key].type)
|
||||||
|
# now process new events
|
||||||
for event in pygame.event.get():
|
for event in pygame.event.get():
|
||||||
if event.type == pygame.QUIT:
|
match event.type:
|
||||||
self.running = False
|
case pygame.QUIT:
|
||||||
|
self.running = False
|
||||||
|
case pygame.KEYDOWN | pygame.KEYUP:
|
||||||
|
self.handle_key_event(event.type, event.key)
|
||||||
|
|
||||||
|
def swap_with_hold(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def intersects_board(self, x: int, y: int, piece: Tetromino):
|
||||||
|
pw, ph = piece.width, piece.height
|
||||||
|
if x < 0 or y < 0:
|
||||||
|
return True
|
||||||
|
elif x + pw > Game.BOARD_SIZE.width or y + ph > Game.BOARD_SIZE.height:
|
||||||
|
return True
|
||||||
|
shape = piece.shape
|
||||||
|
for cy, row in enumerate(shape):
|
||||||
|
for cx, cell in enumerate(row):
|
||||||
|
if cell and self.board[y + cy][x + cx]:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def rotate_current_piece(self, times: int):
|
||||||
|
new_piece = self.current_block.rotated(times)
|
||||||
|
cx, cy = self.current_block_pos
|
||||||
|
for dx in [0, -1]:
|
||||||
|
for dy in [1, 0, -1]:
|
||||||
|
np = Cell(cx + dx, cy + dy)
|
||||||
|
if not self.intersects_board(*np, new_piece):
|
||||||
|
self.current_block = new_piece
|
||||||
|
self.current_block_pos = np
|
||||||
|
return
|
||||||
|
|
||||||
|
def move_current_piece(self, dir: Direction):
|
||||||
|
dx = dir * Game.MOVE_SPEED / Game.FRAMERATE
|
||||||
|
self.subcell_move += dx
|
||||||
|
move_cell = int(self.subcell_move)
|
||||||
|
self.subcell_move -= move_cell
|
||||||
|
if move_cell:
|
||||||
|
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.current_block_pos = new_pos
|
||||||
|
|
||||||
|
def process_actions(self):
|
||||||
|
if not (
|
||||||
|
{Action.Type.MOVE_LEFT, Action.Type.MOVE_RIGHT}
|
||||||
|
& set(self.pending_actions)
|
||||||
|
):
|
||||||
|
self.subcell_move = 0
|
||||||
|
while self.pending_actions:
|
||||||
|
match self.pending_actions.pop(0):
|
||||||
|
case Action.Type.ROTATE_LEFT:
|
||||||
|
self.rotate_current_piece(Direction.LEFT)
|
||||||
|
case Action.Type.ROTATE_RIGHT:
|
||||||
|
self.rotate_current_piece(Direction.RIGHT)
|
||||||
|
case Action.Type.SWAP_HOLD:
|
||||||
|
self.swap_with_hold()
|
||||||
|
case Action.Type.DROP:
|
||||||
|
self.doing_drop = True
|
||||||
|
case Action.Type.MOVE_LEFT:
|
||||||
|
self.move_current_piece(Direction.LEFT)
|
||||||
|
case Action.Type.MOVE_RIGHT:
|
||||||
|
self.move_current_piece(Direction.RIGHT)
|
||||||
|
|
||||||
|
def place_piece(self, x: int, y: int, piece: Tetromino):
|
||||||
|
for dy, row in enumerate(piece.shape):
|
||||||
|
for dx, cell in enumerate(row):
|
||||||
|
if cell:
|
||||||
|
self.board[y + dy][x + dx] = piece.color
|
||||||
|
|
||||||
|
def advance_piece(self):
|
||||||
|
speed = (
|
||||||
|
Game.FAST_DROP_SPEED if self.doing_drop else Game.NORMAL_DROP_SPEED
|
||||||
|
)
|
||||||
|
self.subcell_drop += speed / Game.FRAMERATE
|
||||||
|
move_cell = int(self.subcell_drop)
|
||||||
|
self.subcell_drop -= move_cell
|
||||||
|
cp = self.current_block_pos
|
||||||
|
if not self.intersects_board(
|
||||||
|
cp.x, cp.y + move_cell, self.current_block
|
||||||
|
):
|
||||||
|
self.current_block_pos = Cell(cp.x, cp.y + move_cell)
|
||||||
|
else:
|
||||||
|
for dy in range(move_cell, -1, -1):
|
||||||
|
if not self.intersects_board(
|
||||||
|
cp.x, cp.y + dy, self.current_block
|
||||||
|
):
|
||||||
|
self.place_piece(cp.x, cp.y + dy, self.current_block)
|
||||||
|
self.current_block = None
|
||||||
|
return
|
||||||
|
# TODO Game over
|
||||||
|
print("Game over!")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_clear_cells():
|
||||||
|
bw = Game.BOARD_SIZE.width
|
||||||
|
if bw % 2 == 0:
|
||||||
|
return zip(range(bw // 2 - 1, -1, -1), range(bw // 2, bw))
|
||||||
|
else:
|
||||||
|
return zip(range(bw // 2, -1, -1), range(bw // 2, bw))
|
||||||
|
|
||||||
|
def maybe_clear_rows(self):
|
||||||
|
def do_single_clear(y: int):
|
||||||
|
row = self.board[y]
|
||||||
|
for x1, x2 in Game.generate_clear_cells():
|
||||||
|
if row[x1]:
|
||||||
|
row[x1] = None
|
||||||
|
row[x2] = None
|
||||||
|
break
|
||||||
|
|
||||||
|
def drop_rows_above_for_clear(y: int):
|
||||||
|
self.board[: y + 1] = self.board[:y]
|
||||||
|
self.board.insert(0, [None] * Game.BOARD_SIZE.width)
|
||||||
|
|
||||||
|
if not self.clearing_rows:
|
||||||
|
for y, row in enumerate(self.board):
|
||||||
|
if all(row):
|
||||||
|
self.clearing_rows.add(y)
|
||||||
|
elif not any(self.board[next(iter(self.clearing_rows))]):
|
||||||
|
for y in self.clearing_rows:
|
||||||
|
drop_rows_above_for_clear(y)
|
||||||
|
self.clearing_rows = set()
|
||||||
|
self.clearing_frames = self.CLEAR_SPEED
|
||||||
|
else:
|
||||||
|
if self.clearing_frames == self.CLEAR_SPEED:
|
||||||
|
self.clearing_frames = 0
|
||||||
|
for y in self.clearing_rows:
|
||||||
|
do_single_clear(y)
|
||||||
|
else:
|
||||||
|
self.clearing_frames += 1
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
self.running = True
|
self.running = True
|
||||||
while self.running:
|
while self.running:
|
||||||
|
self.doing_drop = False
|
||||||
|
|
||||||
self.handle_events()
|
self.handle_events()
|
||||||
|
|
||||||
|
self.maybe_clear_rows()
|
||||||
|
if not self.clearing_rows:
|
||||||
|
self.process_actions()
|
||||||
|
self.advance_piece()
|
||||||
|
else:
|
||||||
|
# ignore input while clearing
|
||||||
|
self.pending_actions = []
|
||||||
|
|
||||||
|
if not self.current_block:
|
||||||
|
self.generate_new_block()
|
||||||
|
|
||||||
self.screen.fill("black")
|
self.screen.fill("black")
|
||||||
self.draw_board_border()
|
if not self.clearing_rows:
|
||||||
|
self.draw_current_block()
|
||||||
self.draw_board_content()
|
self.draw_board_content()
|
||||||
self.draw_current_block()
|
self.draw_board_border()
|
||||||
|
|
||||||
pygame.display.flip()
|
pygame.display.flip()
|
||||||
self.clock.tick(Game.FRAMERATE)
|
self.clock.tick(Game.FRAMERATE)
|
||||||
|
|
||||||
@@ -210,13 +451,23 @@ class Game:
|
|||||||
|
|
||||||
def init(self):
|
def init(self):
|
||||||
pygame.init()
|
pygame.init()
|
||||||
self.screen = pygame.display.set_mode(Game.WINDOW_SIZE)
|
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()
|
||||||
|
|
||||||
|
pygame.key.stop_text_input()
|
||||||
|
self.key_states = defaultdict(lambda: False)
|
||||||
|
self.pending_actions = []
|
||||||
|
|
||||||
# Game state
|
# Game state
|
||||||
self.board = [[None] * Game.BOARD_SIZE.width] * Game.BOARD_SIZE.height
|
self.board = make_matrix(*Game.BOARD_SIZE)
|
||||||
self.generate_new_block()
|
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