![]() |
|
[Pyglet] Making tetrominos - Printable Version +- Python Forum (https://python-forum.io) +-- Forum: Python Coding (https://python-forum.io/forum-7.html) +--- Forum: Game Development (https://python-forum.io/forum-11.html) +--- Thread: [Pyglet] Making tetrominos (/thread-40740.html) Pages:
1
2
|
RE: Making tetrominos - ragoo - Sep-26-2023 See, to me tetrominoes and minos should only know how to rotate and shift position. Neither of them should know about the field. import math
import random
from pyglet.shapes import Rectangle
from pyglet.math import Vec2
import settings
class Shape:
"""I know the details of a tetronimo shape.
Creating an instance of Shape adds the shape to my dictionary. Use
get(id) to get the Shape instance that matches the id, or use random()
go get a random shape.
"""
dictionary = {} # Lookup Shape by id.
default_kick = (
{3: ((-1, 0), (-1, -1), (0, 2), (-1, 2)), 1: ((1, 0), (1, -1), (0, 2), (1, 2))},
{0: ((-1, 0), (-1, 1), (0, -2), (-1, -2)), 2: ((-1, 0), (-1, 1), (0, -2), (-1, -2))},
{1: ((1, 0), (1, -1), (0, 2), (1, 2)), 3: ((-1, 0), (-1, -1), (0, 2), (-1, 2))},
{2: ((1, 0), (1, 1), (0, -2), (1, -2)), 0: ((1, 0), (1, 1), (0, -2), (1, -2))},
)
default_pivot = (-1, -2)
def __init__(self, id, color, minos, pivot=None, wall_kick=None):
pivot = self.default_pivot if pivot is None else pivot
wall_kick = self.default_kick if wall_kick is None else wall_kick
self.id = id
self.color = color
self.minos = [Vec2(*mino) for mino in minos]
self.pivot = Vec2(*pivot)
self.wall_kick = wall_kick
self.dictionary[id] = self
@classmethod
def get(cls, key):
"""Lookup shape by id."""
return cls.dictionary[key]
@classmethod
def random(cls):
"""Return random shape."""
return random.choice(list(cls.dictionary.values()))
class Mino(Rectangle):
cell_size = 10
def __init__(self, grid_position, pivot, color, batch):
self.grid_position = grid_position
self.pivot = pivot
x = self.grid_position.x * self.cell_size
y = self.grid_position.y * self.cell_size
super().__init__(x, y, self.cell_size, self.cell_size, color, batch)
def rotate(self, angle):
translated = self.grid_position - self.pivot
rotated = translated.rotate(math.radians(angle))
self.grid_position = Vec2(*map(round, rotated + self.pivot))
self.update()
def shift(self, vector):
self.pivot += vector
self.grid_position += vector
self.update()
def update(self):
self.x = self.grid_position.x * self.width
self.y = self.grid_position.y * self.height
class Tetromino:
"""I am a tetronimo that the player is moving on the screen.
Use random(location, rotation) to create a randomly shaped
tetronimo. location is the coordinates of the origin. Rotation
is 0..3.
Use get(shape_id, location, rotation) to create a tetronimo
of a specific shape.
"""
@classmethod
def random(cls, location, batch):
"""Create a random Tetronimo."""
return cls(Shape.random(), location, batch)
@classmethod
def get(cls, shape_id, location, batch):
"""Create Tetronimo using shape with matching id."""
return cls(Shape.get(shape_id), location, batch)
def __init__(self, shape, location, batch):
self.minos = [
Mino(pos + location, shape.pivot + location, shape.color, batch) for pos in shape.minos
]
self.wall_kicks = shape.wall_kick
self.rotation = 0
self.old_rotation = 0
def get_positions(self):
return (mino.grid_position for mino in self.minos)
def get_wall_kicks(self):
return self.wall_kicks[self.rotation][self.old_rotation]
def rotate(self, angle):
"""Rotate tetronimo."""
self.old_rotation = self.rotation
self.rotation = (self.rotation - int(math.copysign(1, angle))) % 4
for mino in self.minos:
mino.rotate(angle)
def shift(self, vector):
"""Move in x and y direction."""
for mino in self.minos:
mino.shift(vector)
# Define all the shapes
Shape("I", settings.icolor, ((-2, -2), (-1, -2), (0, -2), (1, -2)),
pivot=(-0.5, -2.5),
wall_kick=(
{3: ((1, 0), (-2, 0), (1, -2), (-2, 1)), 1: ((2, 0), (-1, 0), (2, 1), (-1, -2))},
{0: ((-2, 0), (1, 0), (-2, -1), (1, 2)), 2: ((1, 0), (-2, 0), (1, -2), (-2, 1))},
{1: ((-1, 0), (2, 0), (-1, 2), (2, -1)), 3: ((-2, 0), (1, 0), (-2, -1), (1, 2))},
{2: ((2, 0), (-1, 0), (2, 1), (-1, -2)), 0: ((-1, 0), (2, 0), (-1, 2), (2, -1))}
)
)
Shape("O", settings.ocolor, ((-1, -2), (0, -2), (-1, -1), (0, -1)), (-0.5, -1.5), [])
Shape("J", settings.jcolor, ((-2, -2), (-1, -2), (0, -2), (-2, -1)))
Shape("L", settings.lcolor, ((-2, -2), (-1, -2), (0, -2), (0, -1)))
Shape("S", settings.scolor, ((-2, -2), (-1, -2), (-1, -1), (0, -1)))
Shape("T", settings.tcolor, ((-2, -2), (-1, -2), (0, -2), (-1, -1)))
Shape("Z", settings.zcolor, ((-1, -2), (0, -2), (-2, -1), (-1, -1)))The field should not know about the minos, as long as they're in control of the player:class Field:
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = [[None for x in range(width)] for y in range(height)]
def add_mino(self, mino):
self.grid[mino.grid_position.y][mino.grid_position.x] = mino
def add_minos(self, minos):
for mino in minos:
self.add_mino(mino)
def is_cell_free(self, position):
if 0 <= position.x < self.width and 0 <= position.y < self.height:
if not self.grid[position.y][position.x]:
return True
return False
def are_cells_free(self, positions):
return all(map(self.is_cell_free, positions))In the input_handler module I can provide an interface that both the InputHandler and the Tetris classes can use, through the Command pattern. The thing is, in a Tetris game there are two entities that control a tetromino: the player, trough keyboard (or joypad, or whatever) inputs, and the game itself, which, at least, is supposed to be moving the tetromino down at a speed set by certain game conditions (i.e. every 10 lines cleared the speed increases), but in the case of modern Tetris, it must also perform wall kicks (and the Tetromino class already provides the shift method to pull them off). And, furthermore, the thing about the Command pattern is that it allows me to save a move and undo it if the tetromino fails the collision check.from pyglet.math import Vec2
from pyglet.window import key
class Command:
def execute(self):
pass
def undo(self):
pass
class Rotate(Command):
def __init__(self, tetromino, angle):
self.tetromino = tetromino
self.angle = angle
def execute(self):
self.tetromino.rotate(self.angle)
def undo(self):
self.tetromino.rotate(-self.angle)
class Shift(Command):
def __init__(self, tetromino, vector):
self.tetromino = tetromino
self.vector = vector
def execute(self):
self.tetromino.shift(self.vector)
def undo(self):
self.tetromino.shift(-self.vector)
class InputHandler:
def __init__(
self,lx=key.LEFT, rx=key.RIGHT, soft_drop=key.DOWN,
rotate_acw=key.Z, rotate_cw=key.X):
"""Welcome to hell."""
self.key_state = key.KeyStateHandler()
# Key mapping
self.lx = lx
self.rx = rx
self.soft_drop = soft_drop
self.rotate_acw = rotate_acw
self.rotate_cw = rotate_cw
self.lx_delay_timer = 0
self.rx_delay_timer = 0
self.lx_repeat_timer = 0
self.rx_repeat_timer = 0
self.soft_drop_delay_timer = 0
self.soft_drop_repeat_timer = 0
self.lx_was_pressed = False
self.rx_was_pressed = False
self.soft_drop_was_pressed = False
self.rotate_acw_was_pressed = False
self.rotate_cw_was_pressed = False
def handle_x_movement(self, tetromino, dt, delay=0.08, repeat=0.05):
if self.key_state[self.lx] and not self.rx_was_pressed:
if self.lx_was_pressed:
if self.lx_delay_timer >= delay:
if self.lx_repeat_timer >= repeat:
self.lx_repeat_timer = 0
return Shift(tetromino, Vec2(-1, 0))
else:
self.lx_repeat_timer += dt
else:
self.lx_delay_timer += dt
else:
self.lx_was_pressed = True
return Shift(tetromino, Vec2(-1, 0))
else:
self.lx_was_pressed = False
self.lx_delay_timer = 0
self.lx_repeat_timer = 0
if self.key_state[self.rx] and not self.lx_was_pressed:
if self.rx_was_pressed:
if self.rx_delay_timer >= delay:
if self.rx_repeat_timer >= repeat:
self.rx_repeat_timer = 0
return Shift(tetromino, Vec2(1, 0))
else:
self.rx_repeat_timer += dt
else:
self.rx_delay_timer += dt
else:
self.rx_was_pressed = True
return Shift(tetromino, Vec2(1, 0))
else:
self.rx_was_pressed = False
self.rx_delay_timer = 0
self.rx_repeat_timer = 0
def handle_y_movement(self, tetromino, dt, delay=0.08, repeat=0.05):
if self.key_state[self.soft_drop]:
if self.soft_drop_was_pressed:
if self.soft_drop_delay_timer >= delay:
if self.soft_drop_repeat_timer >= repeat:
self.soft_drop_repeat_timer = 0
return Shift(tetromino, Vec2(0, -1))
else:
self.soft_drop_repeat_timer += dt
else:
self.soft_drop_delay_timer += dt
else:
self.soft_drop_was_pressed = True
return Shift(tetromino, Vec2(0, -1))
else:
self.soft_drop_was_pressed = False
self.soft_drop_delay_timer = 0
self.soft_drop_repeat_timer = 0
def handle_rotation(self, tetromino):
if self.key_state[self.rotate_acw] and not self.rotate_cw_was_pressed:
if not self.rotate_acw_was_pressed:
self.rotate_acw_was_pressed = True
return Rotate(tetromino, 90)
else:
self.rotate_acw_was_pressed = False
if self.key_state[self.rotate_cw] and not self.rotate_acw_was_pressed:
if not self.rotate_cw_was_pressed:
self.rotate_cw_was_pressed = True
return Rotate(tetromino, -90)
else:
self.rotate_cw_was_pressed = FalseNow, the Tetris class is the one that manages the entire game logic. And it serves as an intermediary between the field and the tetrominoes:import pyglet
from pyglet.math import Vec2
import input_handler
from field import Field
from tetromino import Tetromino
class Tetris:
field_width = 10
field_height = 20
def __init__(self, batch):
self.batch = batch
self.input_handler = input_handler.InputHandler()
self.field = Field(self.field_width, self.field_height)
self.new_tetromino()
self.tetromino_has_landed = False
def new_tetromino(self):
position = Vec2(self.field_width // 2, self.field_height)
self.tetromino = Tetromino.random(position, self.batch)
def update(self, dt):
x_movement = self.input_handler.handle_x_movement(self.tetromino, dt)
if x_movement:
x_movement.execute()
if not self.field.are_cells_free(self.tetromino.get_positions()):
x_movement.undo()
y_movement = self.input_handler.handle_y_movement(self.tetromino, dt)
if y_movement:
y_movement.execute()
if not self.field.are_cells_free(self.tetromino.get_positions()):
y_movement.undo()
self.tetromino_has_landed = True
rotation = self.input_handler.handle_rotation(self.tetromino)
if rotation:
rotation.execute()
if not self.field.are_cells_free(self.tetromino.get_positions()):
wall_kicks = (input_handler.Shift(self.tetromino, Vec2(x, y)) for x, y in self.tetromino.get_wall_kicks())
for wall_kick in wall_kicks:
wall_kick.execute()
if self.field.are_cells_free(self.tetromino.get_positions()):
break
else:
wall_kick.undo()
else:
rotation.undo()
if self.tetromino_has_landed:
self.field.add_minos(self.tetromino.minos)
self.new_tetromino()
self.tetromino_has_landed = FalseWhat do you think about this solution? What I like is that it makes the implementation of wall kicks dirt simple. What I don't like is that these tetrominoes now carry this kick data that is supposed to be used externally, which is probably a bit confusing. I mean, for sure, conceptually they're part of the Tetromino behavior.I think that for the batch problem, I can implement the MVC design pattern, instead of what I'm doing. So, Mino becomes a simple object that only stores grid position and the pivot point, and all the rendering goes into a separate part of the codebase, so that I can use just use a single batch for rendering (I really think pyglet would like me to use a single batch for rendering all I can draw in a window. Multiple batches are probably meant to be used if I have more than one window.) RE: Making tetrominos - Benixon - Dec-07-2023 The game generates a Tetronimo at random. The Tetronimo generates a large number of Minos. When you make a Minos, you get a rectangle. The rectangle floats above the grid rather of being added to it. |