Posts: 6
Threads: 1
Joined: Sep 2023
Sep-15-2023, 11:00 AM
(This post was last modified: Sep-15-2023, 11:02 AM by ragoo.)
Hi. I don't think this is really a game development problem, it's more of a design problem best I can tell.
I've written a base Tetromino class that is functional for every possible shape:
class Tetromino:
def __init__(self, minos, pivot, wall_kick_data):
self.minos = minos
self.pivot = pivot
self.wall_kick_data = wall_kick_data
self.current_rotation = 0
self.previous_rotation = 0
def move_by(self, vector):
self.pivot += vector
for mino in self.minos:
mino.move_by(vector)
def rotate(self, angle):
self.previous_rotation = self.current_rotation
if angle > 0:
self.current_rotation = (self.current_rotation - 1) % 4
else:
self.current_rotation = (self.current_rotation + 1) % 4
for mino in self.minos:
mino.rotate(self.pivot, angle)
def update(self):
for mino in self.minos:
mino.update() As you've probably noticed, it contains a list of minos objects (apparently that's the official name for the blocks that compose a tetromino), which in turn are a subclass of the pyglet.shape.Rectangle class:
from pyglet.shapes import Rectangle
from pyglet.math import Vec2
class Mino(Rectangle):
def __init__(self, *pargs, **kargs):
super().__init__(*pargs, **kargs)
self.grid_position = Vec2(self.x // self.width, self.y // self.height)
def move_by(self, vector):
self.grid_position += vector
def rotate(self, pivot, angle):
translated = self.grid_position - pivot
rotated = translated.rotate(angle)
self.grid_position = Vec2(*map(round, rotated + pivot))
def update(self):
self.x = self.grid_position.x * self.width
self.y = self.grid_position.y * self.height Now, this is all well and good, by my standards at least. The problem is when it comes to actually build these tetrominos according to their shape. The client is going to pick up a random shape for the next tetromino whenever it's time to spawn a new one.
The sheer amount of data I need to sort through is overwhelming though.
For each shape I have: - the shape data, a tuple of grid coordinates for each mino.
shapes_data = {
'I': ((-2, -2), (-1, -2), (0, -2), (1, -2)),
'J': ((-2, -2), (-1, -2), (0, -2), (-2, -1)),
'L': ((-2, -2), (-1, -2), (0, -2), (0, -1)),
'O': ((-1, -2), (0, -2), (-1, -1), (0, -1)),
'S': ((-2, -2), (-1, -2), (-1, -1), (0, -1)),
'T': ((-2, -2), (-1, -2), (0, -2), (-1, -1)),
'Z': ((-1, -2), (0, -2), (-2, -1), (-1, -1))} Note that this just gives me the shape, then I actually need to translate them to the right place, adding each one of them to (grid.width // 2, grid.height).
- the pivot point for each shape
shapes_pivots = {
'I': (-0.5, -2.5),
'J': (-1, -2),
'L': (-1, -2),
'O': (-0.5, -1.5),
'S': (-1, -2),
'T': (-1, -2),
'Z': (-1, -2)} This also needs to be translated to the actual spawn point, adding it to (grid.width // 2, grid.height).
- the wall kick data. This one is nasty. It contains a list of coordinates the tetromino is supposed to be moved by, according to its rotation states, whenever a rotation fails (because of an obstacle, either the walls or tetrominos in the grid). Anyway, the logic is not the problem, I got that.
standard_wall_kicks_data = {
0: {
3: ((-1, 0), (-1, -1), (0, 2), (-1, 2)),
1: ((1, 0), (1, -1), (0, 2), (1, 2))},
1: {
0: ((-1, 0), (-1, 1), (0, -2), (-1, -2)),
2: ((-1, 0), (-1, 1), (0, -2), (-1, -2))},
2: {
1: ((1, 0), (1, -1), (0, 2), (1, 2)),
3: ((-1, 0), (-1, -1), (0, 2), (-1, 2))},
3: {
2: ((1, 0), (1, 1), (0, -2), (1, -2)),
0: ((1, 0), (1, 1), (0, -2), (1, -2))}
}
i_wall_kicks_data = {
0: {
3: ((1, 0), (-2, 0), (1, -2), (-2, 1)),
1: ((2, 0), (-1, 0), (2, 1), (-1, -2))},
1: {
0: ((-2, 0), (1, 0), (-2, -1), (1, 2)),
2: ((1, 0), (-2, 0), (1, -2), (-2, 1))},
2: {
1: ((-1, 0), (2, 0), (-1, 2), (2, -1)),
3: ((-2, 0), (1, 0), (-2, -1), (1, 2))},
3: {
2: ((2, 0), (-1, 0), (2, 1), (-1, -2)),
0: ((-1, 0), (2, 0), (-1, 2), (2, -1))}
}
shapes_wall_kicks_data = {
'I': i_wall_kicks_data,
'J': standard_wall_kicks_data,
'L': standard_wall_kicks_data,
'O': standard_wall_kicks_data,
'S': standard_wall_kicks_data,
'T': standard_wall_kicks_data,
'Z': standard_wall_kicks_data} Notice how I is the only tetromino that follows a different ruleset.
- finally colors:
colors = {
'I': i_color,
'J': j_color,
'L': l_color,
'O': o_color,
'S': s_color,
'T': t_color,
'Z': z_color}
Now, my idea was to create a class whose job is to make tetrominos for the client. And I came up with this juggernaut:
from pyglet.math import Vec2
from mino import Mino
from tetromino import Tetromino
shapes_data = {
'I': ((-2, -2), (-1, -2), (0, -2), (1, -2)),
'J': ((-2, -2), (-1, -2), (0, -2), (-2, -1)),
'L': ((-2, -2), (-1, -2), (0, -2), (0, -1)),
'O': ((-1, -2), (0, -2), (-1, -1), (0, -1)),
'S': ((-2, -2), (-1, -2), (-1, -1), (0, -1)),
'T': ((-2, -2), (-1, -2), (0, -2), (-1, -1)),
'Z': ((-1, -2), (0, -2), (-2, -1), (-1, -1))}
shapes_pivots = {
'I': (-0.5, -2.5),
'J': (-1, -2),
'L': (-1, -2),
'O': (-0.5, -1.5),
'S': (-1, -2),
'T': (-1, -2),
'Z': (-1, -2)}
standard_wall_kicks_data = {
0: {
3: ((-1, 0), (-1, -1), (0, 2), (-1, 2)),
1: ((1, 0), (1, -1), (0, 2), (1, 2))},
1: {
0: ((-1, 0), (-1, 1), (0, -2), (-1, -2)),
2: ((-1, 0), (-1, 1), (0, -2), (-1, -2))},
2: {
1: ((1, 0), (1, -1), (0, 2), (1, 2)),
3: ((-1, 0), (-1, -1), (0, 2), (-1, 2))},
3: {
2: ((1, 0), (1, 1), (0, -2), (1, -2)),
0: ((1, 0), (1, 1), (0, -2), (1, -2))}
}
i_wall_kicks_data = {
0: {
3: ((1, 0), (-2, 0), (1, -2), (-2, 1)),
1: ((2, 0), (-1, 0), (2, 1), (-1, -2))},
1: {
0: ((-2, 0), (1, 0), (-2, -1), (1, 2)),
2: ((1, 0), (-2, 0), (1, -2), (-2, 1))},
2: {
1: ((-1, 0), (2, 0), (-1, 2), (2, -1)),
3: ((-2, 0), (1, 0), (-2, -1), (1, 2))},
3: {
2: ((2, 0), (-1, 0), (2, 1), (-1, -2)),
0: ((-1, 0), (2, 0), (-1, 2), (2, -1))}
}
shapes_wall_kicks_data = {
'I': i_wall_kicks_data,
'J': standard_wall_kicks_data,
'L': standard_wall_kicks_data,
'O': standard_wall_kicks_data,
'S': standard_wall_kicks_data,
'T': standard_wall_kicks_data,
'Z': standard_wall_kicks_data}
class StandardRotationSystem:
def __init__(self):
self.shapes_data = shapes_data
self.shapes_pivots = shapes_pivots
self.shapes_wall_kicks_data = shapes_wall_kicks_data
def _get_minos(self, shape, dx, dy, cell_size, color, batch):
minos = list()
positions = (((x + dx) * cell_size, (y + dy) * cell_size) for (x, y) in self.shapes_data[shape])
for pos in positions:
mino = Mino(*pos, cell_size, cell_size, color, batch)
minos.append(mino)
return minos
def _get_pivot(self, shape, dx, dy):
pivot = Vec2(*self.shapes_pivots[shape]) + Vec2(dx, dy)
return pivot
def _get_wall_kick_data(self, shape):
return self.shapes_wall_kicks_data[shape]
def make_tetromino(self, shape, dx, dy, cell_size, color, batch):
minos = self._get_minos(shape, dx, dy, cell_size, color, batch)
pivot = self._get_pivot(shape, dx, dy)
wall_kick_data = self._get_wall_kick_data(shape)
return Tetromino(minos, pivot, wall_kick_data) But, c'mon, this can't be the right way to do this. What's the best way to tame this complexity?
Posts: 6,803
Threads: 20
Joined: Feb 2020
Sep-15-2023, 03:34 PM
(This post was last modified: Sep-15-2023, 03:34 PM by deanhystad.)
I think you might be overthinking the problem. The Tetris playing field is a grid. Moving and rotating a tetronimo is computing the grid location for each of the tetronimo minos. A move is valid if all the minos are within the grid and if none of the minos collide with another minos. Collisions are detected by checking if the cell wheren the minos wants to move is empty or full.
import random
class Field:
def __init__(self, wide, high):
self.wide = wide
self.high = high
self.grid = [["_"] * wide for _ in range(high)]
def set(self, x, y, color="_"):
self.grid[y][x] = color
def get(self, x, y):
if 0 <= x < self.wide and 0 <= y < self.high:
return self.grid[y][x]
return "X"
def __str__(self):
return "\n".join(["".join(row) for row in self.grid])
class Minos:
rotations = [(1, 0), (0, 1), (-1, 0), (0, -1)]
field = None
def __init__(self, color, position):
self.color = color
self.position = position
self.location = []
def compute(self, location, rotation):
rcos, rsin = self.rotations[rotation]
lx, ly = location
px, py = self.position
self.location = [lx + px * rcos + py * rsin, ly + px * rsin - py * rcos]
return field.get(*self.location) == "_"
def draw(self):
self.field.set(*self.location, self.color)
def erase(self):
self.field.set(*self.location)
class Tetronimo:
def __init__(self, shape, location, rotation):
self.location = location
self.rotation = rotation
self.minos = [Minos(shape["color"], pos) for pos in shape["position"]]
self.compute(location, rotation)
self.draw()
def move(self, shift, rotation):
self.erase()
location = (self.location[0] + shift[0], self.location[1] + shift[1])
rotation = (self.rotation + rotation) % 4
if self.compute(location, rotation):
self.location = location
self.rotation = rotation
else:
self.compute(self.location, self.rotation)
self.draw()
def draw(self):
for minos in self.minos:
minos.draw()
def erase(self):
for minos in self.minos:
minos.erase()
def __str__(self):
return f"<Tetronimo {self.location}, {self.rotation}, {[minos.location for minos in self.minos]}>"
def compute(self, location, rotation):
return all(m.compute(location, rotation) for m in self.minos)
shapes = [
{"color": "R", "position": ((0, 0), (1, 0), (2, 0), (2, 1))},
]
field = Field(5, 5)
Minos.field = field
x = Tetronimo(random.choice(shapes), [2, 2], 0)
print(x, field, sep="\n")
for _ in range(4):
x.move((0, 0), 1)
print(x, field, sep="\n")
for _ in range(3):
x.move((0, 1), 0)
print(x, field, sep="\n") Output: <Tetronimo [2, 2], 0, [[2, 2], [3, 2], [4, 2], [4, 1]]>
_____
____R
__RRR
_____
_____
<Tetronimo (2, 2), 1, [[2, 2], [2, 3], [2, 4], [3, 4]]>
_____
_____
__R__
__R__
__RR_
<Tetronimo (2, 2), 2, [[2, 2], [1, 2], [0, 2], [0, 3]]>
_____
_____
RRR__
R____
_____
<Tetronimo (2, 2), 3, [[2, 2], [2, 1], [2, 0], [1, 0]]>
_RR__
__R__
__R__
_____
_____
<Tetronimo (2, 2), 0, [[2, 2], [3, 2], [4, 2], [4, 1]]>
_____
____R
__RRR
_____
_____
<Tetronimo (2, 3), 0, [[2, 3], [3, 3], [4, 3], [4, 2]]>
_____
_____
____R
__RRR
_____
<Tetronimo (2, 4), 0, [[2, 4], [3, 4], [4, 4], [4, 3]]>
_____
_____
_____
____R
__RRR
<Tetronimo (2, 4), 0, [[2, 4], [3, 4], [4, 4], [4, 3]]>
_____
_____
_____
____R
__RRR
Posts: 6
Threads: 1
Joined: Sep 2023
The thing is I'm more of a fan of modern Tetris, which means the Standard Rotation System, or Super Rotation System (SRS).
https://tetris.fandom.com/wiki/SRS
The SRS has pretty strict rules when it comes to tetrominoes rotation ("when unobstructed, the tetrominoes all appear to rotate purely about a single point", hence the different pivots in my implementation). And it features these wall kicks ("when a rotation is attempted, 5 positions are sequentially tested (inclusive of basic rotation); if none are available, the rotation fails completely."), which allow to perform pretty dope moves like the T-Spin, and frankly it feels more natural compared to other Tetris versions without wall kicks, like the classic NES and Game Boy versions.
https://tetris.com/play-tetris
This version features the SRS with wall kicks. You can easily see some of them in action by rotating tetrominoes against the edges of the field.
I've already implemented them in the game loop (actually this was supposed to be a follow up question, that is, where to put what where), and they seem to work fine, but I think my code is starting to become a mess, so I need to sort things out first.
Posts: 6,803
Threads: 20
Joined: Feb 2020
Sep-15-2023, 11:11 PM
(This post was last modified: Sep-15-2023, 11:11 PM by deanhystad.)
Instead of a bunch of dictionaries, I would use a class to collect all the information about a shape into one object.
import random
class Shape:
"""I know the details of a tetronimo shape."""
instances = [] # List of all Shape objects
dictionary = {} # Lookup Shape by id.
def __init__(self, id, origin, monos, wall_kick):
self.id = id # What shape am I
self.color = len(self.instances) # Index into a color table. For different themes?
self.origin = origin # Origin I rotate about
self.monos = monos # List of mono coordinates
self.wall_kick = wall_kick # Wall kick information for SRS
self.dictionary[id] = self
self.instances.append(self)
@classmethod
def get(cls, key):
"""Lookup shape by id."""
return cls.dictionary[key]
@classmethod
def random(cls):
"""Return random shape."""
return random.choice(cls.instances)
class Mino:
"""I am one block in a tetronimo."""
def __init__(self, color, position):
pass
class Tetronimo:
"""I am a tetronimo that the player is moving on the screen."""
starting_location = (50, 3)
@classmethod
def random(cls):
"""Create a random Tetronimo."""
return cls(Shape.random(), cls.starting_location, random.randint(0, 3))
def __init__(self, shape, location, rotation=0):
self.origin = shape.origin
self.minos = [Mino(shape.color, position) for position in shape.monos]
self.wall_kick = shape.wall_kick
self.location = location
self.rotation = rotation
self.update()
def rotate(self, rotation):
"""Implements standard rotation system."""
def shift(self, x, y):
"""Move in x and y direction."""
def update(self):
"""Update all the mino positions."""
ikick = [
{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))},
]
jkick = [
{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))},
]
Shape("I", (-0.5, -2.5), ((-2, -2), (-1, -2), (0, -2), (1, -2)), ikick),
Shape("J", (-1, -2), ((-2, -2), (-1, -2), (0, -2), (-2, -1)), jkick),
Shape("L", (-1, -2), ((-2, -2), (-1, -2), (0, -2), (0, -1)), jkick),
Shape("O", (-0.5, -1.5), ((-1, -2), (0, -2), (-1, -1), (0, -1)), jkick),
Shape("S", (-1, -2), ((-2, -2), (-1, -2), (-1, -1), (0, -1)), jkick),
Shape("T", (-1, -2), ((-2, -2), (-1, -2), (0, -2), (-1, -1)), jkick),
Shape("Z", (-1, -2), ((-1, -2), (0, -2), (-2, -1), (-1, -1)), jkick),
x = Tetronimo(Shape.random(), (3, 3))
y = Tetronimo.random()
z = Tetronimo(Shape.get("Z"), (3, 3))
Posts: 6
Threads: 1
Joined: Sep 2023
But where do I store the data? In the shape module?
What if I use the simple factory pattern instead?
from pyglet.math import Vec2
from mino import Mino
standard_wall_kick_data = {
0: {
3: ((-1, 0), (-1, -1), (0, 2), (-1, 2)),
1: ((1, 0), (1, -1), (0, 2), (1, 2))},
1: {
0: ((-1, 0), (-1, 1), (0, -2), (-1, -2)),
2: ((-1, 0), (-1, 1), (0, -2), (-1, -2))},
2: {
1: ((1, 0), (1, -1), (0, 2), (1, 2)),
3: ((-1, 0), (-1, -1), (0, 2), (-1, 2))},
3: {
2: ((1, 0), (1, 1), (0, -2), (1, -2)),
0: ((1, 0), (1, 1), (0, -2), (1, -2))}
}
i_wall_kick_data = {
0: {
3: ((1, 0), (-2, 0), (1, -2), (-2, 1)),
1: ((2, 0), (-1, 0), (2, 1), (-1, -2))},
1: {
0: ((-2, 0), (1, 0), (-2, -1), (1, 2)),
2: ((1, 0), (-2, 0), (1, -2), (-2, 1))},
2: {
1: ((-1, 0), (2, 0), (-1, 2), (2, -1)),
3: ((-2, 0), (1, 0), (-2, -1), (1, 2))},
3: {
2: ((2, 0), (-1, 0), (2, 1), (-1, -2)),
0: ((-1, 0), (2, 0), (-1, 2), (2, -1))}
}
class Tetromino:
def __init__(self, shape_data, pivot, wall_kick_data, dx, dy, cell_size, color, batch):
self.minos = [Mino(x + dx, y + dy, cell_size, color, batch) for (x, y) in shape_data]
self.pivot = pivot + Vec2(dx, dy)
self.wall_kick_data = wall_kick_data
self.current_rotation = 0
self.previous_rotation = 0
def move_by(self, vector):
self.pivot += vector
for mino in self.minos:
mino.move_by(vector)
def rotate(self, angle):
self.previous_rotation = self.current_rotation
if angle > 0:
self.current_rotation = (self.current_rotation - 1) % 4
else:
self.current_rotation = (self.current_rotation + 1) % 4
for mino in self.minos:
mino.rotate(self.pivot, angle)
def update(self):
for mino in self.minos:
mino.update()
class I(Tetromino):
def __init__(self, dx, dy, cell_size, color, batch):
shape_data = ((-2, -2), (-1, -2), (0, -2), (1, -2))
pivot = Vec2(-0.5, -2.5)
wall_kick_data = i_wall_kick_data
super().__init__(shape_data, pivot, wall_kick_data, dx, dy, cell_size, color, batch)
class J(Tetromino):
def __init__(self, dx, dy, cell_size, color, batch):
shape_data = ((-2, -2), (-1, -2), (0, -2), (-2, -1))
pivot = Vec2(-1, -2)
wall_kick_data = standard_wall_kick_data
super().__init__(shape_data, pivot, wall_kick_data, dx, dy, cell_size, color, batch)
class L(Tetromino):
def __init__(self, dx, dy, cell_size, color, batch):
shape_data = ((-2, -2), (-1, -2), (0, -2), (0, -1))
pivot = Vec2(-1, -2)
wall_kick_data = standard_wall_kick_data
super().__init__(shape_data, pivot, wall_kick_data, dx, dy, cell_size, color, batch)
class O(Tetromino):
def __init__(self, dx, dy, cell_size, color, batch):
shape_data = ((-1, -2), (0, -2), (-1, -1), (0, -1))
pivot = Vec2(-0.5, -1.5)
wall_kick_data = standard_wall_kick_data
super().__init__(shape_data, pivot, wall_kick_data, dx, dy, cell_size, color, batch)
class S(Tetromino):
def __init__(self, dx, dy, cell_size, color, batch):
shape_data = ((-2, -2), (-1, -2), (-1, -1), (0, -1))
pivot = Vec2(-1, -2)
wall_kick_data = standard_wall_kick_data
super().__init__(shape_data, pivot, wall_kick_data, dx, dy, cell_size, color, batch)
class T(Tetromino):
def __init__(self, dx, dy, cell_size, color, batch):
shape_data = ((-2, -2), (-1, -2), (0, -2), (-1, -1))
pivot = Vec2(-1, -2)
wall_kick_data = standard_wall_kick_data
super().__init__(shape_data, pivot, wall_kick_data, dx, dy, cell_size, color, batch)
class Z(Tetromino):
def __init__(self, dx, dy, cell_size, color, batch):
shape_data = ((-1, -2), (0, -2), (-2, -1), (-1, -1))
pivot = Vec2(-1, -2)
wall_kick_data = standard_wall_kick_data
super().__init__(shape_data, pivot, wall_kick_data, dx, dy, cell_size, color, batch)
class TetrominoFactory:
def make_tetromino(self, shape, dx, dy, cell_size, color, batch):
args = (dx, dy, cell_size, color, batch)
match shape:
case 'I':
return I(*args)
case 'J':
return J(*args)
case 'L':
return L(*args)
case 'O':
return O(*args)
case 'S':
return S(*args)
case 'T':
return T(*args)
case 'Z':
return Z(*args)
case _:
raise ValueError(shape) This way I can store the raw data in the Tetromino subclasses. What I don't like is all those arguments I need to build the minos collection that get passed along the calls. But I need them since each Mino is technically a pyglet rectangle.
Posts: 6,803
Threads: 20
Joined: Feb 2020
Sep-17-2023, 02:38 AM
(This post was last modified: Sep-17-2023, 02:39 AM by deanhystad.)
I don't like the subclassing idea. Subclass when you need different attributes. The Tetronimo subclasses all have the same attributes, they just have different values for the attribute. It would be like subclassing int to make a Three class.
I wouldn't make a Shape module. I would put Shape, Mino and Tetronimo in one module. They are tightly coupled and I see no reason to break them out into separate modules. Shapes are created in that module and used by the Tetronimo class. Dont worry about the Shape objects getting definrd multiple times. Python only imports a module once. Subsequent imports reuse the exhisting module object.
tetronimo.py
import random
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.
def __init__(self, id, color, origin, monos, wall_kick):
self.id = id # Key in dictionary.
self.color = color # Color when drawn.
self.origin = origin # Rotation origin.
self.monos = monos # List of mono coordinates.
self.wall_kick = wall_kick # Wall kick dictionary.
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:
"""I am one block in a tetronimo."""
def __init__(self, color, position):
self.color = color
self.position = position
class Tetronimo:
"""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, rotation=0):
"""Create a random Tetronimo."""
return cls(Shape.random(), location, rotation)
@classmethod
def get(cls, shape_id, location, rotation=0):
"""Create Tetronimo using shape with matching id."""
return cls(Shape.get(shape_id), location, rotation)
def __init__(self, shape, location, rotation=0):
self.shape = shape
self.minos = [Mino(shape.color, position) for position in shape.monos]
self.location = location
self.rotation = rotation
self.update()
def __repr__(self):
return f"<Tetronimo {self.shape.id} color={self.shape.color} location={self.location} roation={self.rotation}>"
def rotate(self, rotation):
"""Implements standard rotation system."""
def shift(self, x, y):
"""Move in x and y direction."""
def update(self):
"""Update all the mino positions."""
# The following code only runs the first time the module is imported.
# Set up the wall kick info. This is the default wall kick.
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))}
)
ikick = (
{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))},
)
# Define all the shapes. Adds shapes to Shape dictionary using id as key.
Shape("I", "cyan", (-0.5, -2.5), ((-2, -2), (-1, -2), (0, -2), (1, -2)), ikick)
Shape("J", "blue", (-1, -2), ((-2, -2), (-1, -2), (0, -2), (-2, -1)), kick)
Shape("L", "orange", (-1, -2), ((-2, -2), (-1, -2), (0, -2), (0, -1)), kick)
Shape("O", "yellow", (-0.5, -1.5), ((-1, -2), (0, -2), (-1, -1), (0, -1)), kick)
Shape("S", "green", (-1, -2), ((-2, -2), (-1, -2), (-1, -1), (0, -1)), kick)
Shape("T", "magenta", (-1, -2), ((-2, -2), (-1, -2), (0, -2), (-1, -1)), kick)
Shape("Z", "red", (-1, -2), ((-1, -2), (0, -2), (-2, -1), (-1, -1)), kick) test.py
from tetronimo import Tetronimo
print(Tetronimo.get("S", (0, 0)))
print(Tetronimo.random((0, 0), 3)) Output: <Tetronimo S color=green location=(0, 0) roation=0>
<Tetronimo L color=orange location=(0, 0) roation=3>
The game does't need to know the shapes. When making a Tetronimo the shape could be selected at random. See Tetronimo.random()
Another choice is to leave the tetronimo.py module generic and build the shapes in your game.
test.py
from tetronimo import Tetronimo, Shape
# Set up the wall kick info. This is the default wall kick.
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))}
)
ikick = (
{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))},
)
# Define all the shapes. Adds shapes to Shape dictionary using id as key.
Shape("I", "cyan", (-0.5, -2.5), ((-2, -2), (-1, -2), (0, -2), (1, -2)), ikick)
Shape("J", "blue", (-1, -2), ((-2, -2), (-1, -2), (0, -2), (-2, -1)), kick)
Shape("L", "orange", (-1, -2), ((-2, -2), (-1, -2), (0, -2), (0, -1)), kick)
Shape("O", "yellow", (-0.5, -1.5), ((-1, -2), (0, -2), (-1, -1), (0, -1)), kick)
Shape("S", "green", (-1, -2), ((-2, -2), (-1, -2), (-1, -1), (0, -1)), kick)
Shape("T", "magenta", (-1, -2), ((-2, -2), (-1, -2), (0, -2), (-1, -1)), kick)
Shape("Z", "red", (-1, -2), ((-1, -2), (0, -2), (-2, -1), (-1, -1)), kick)
print(Tetronimo.get("S", (0, 0)))
print(Tetronimo.random((0, 0), 3)) I have no experience with pyglet. That's why the Mino class is so spare. Some structural changes may be required to fit into the framework defined by pyglet.
Posts: 6
Threads: 1
Joined: Sep 2023
Sep-18-2023, 10:09 AM
(This post was last modified: Sep-18-2023, 10:44 AM by ragoo.)
Okay, thanks, it works perfectly. I've made a few changes:
import math
import random
from pyglet.shapes import Rectangle
from pyglet.math import Vec2
import settings
class Mino(Rectangle):
def __init__(self, grid_position, cell_size, color, batch):
self.grid_position = grid_position
x = self.grid_position.x * cell_size
y = self.grid_position.y * cell_size
super().__init__(x, y, cell_size, cell_size, color, batch)
def rotate(self, pivot, angle):
translated = self.grid_position - pivot
rotated = translated.rotate(math.radians(angle))
self.grid_position = Vec2(*map(round, rotated + pivot))
def shift(self, vector):
self.grid_position += vector
def update(self):
self.x = self.grid_position.x * self.width
self.y = self.grid_position.y * self.height
class Tetromino:
"""I am a tetromino that the player is moving on the screen.
Use random(location, rotation) to create a randomly shaped
tetromimo. location is the coordinates of the origin. Rotation
is 0..3.
Use get(shape_id, offset, rotation) to create a tetromino
of a specific shape.
"""
@classmethod
def random(cls, offset, cell_size, batch, rotation=0):
"""Create a random Tetronimo."""
return cls(Shape.random(), offset, cell_size, batch, rotation)
@classmethod
def get(cls, shape_id, offset, cell_size, batch, rotation=0):
"""Create Tetronimo using shape with matching id."""
return cls(Shape.get(shape_id), offset, cell_size, batch, rotation)
def __init__(self, shape, offset, cell_size, batch, rotation=0):
self.shape = shape
self.minos = [Mino(position, cell_size, shape.color, batch) for position in shape.minos]
self.location = shape.origin
self.rotation = rotation
self.shift(offset)
self.update()
def __repr__(self):
return f"<Tetronimo {self.shape.id} color={self.shape.color} location={self.location} rotation={self.rotation}>"
def rotate(self, angle):
"""Implements standard rotation system."""
self.rotation = (self.rotation - int(math.copysign(1, angle))) % 4
for mino in self.minos:
mino.rotate(self.location, angle)
def shift(self, vector):
"""Move in x and y direction."""
self.location += vector
for mino in self.minos:
mino.shift(vector)
def update(self):
"""Update all the mino positions."""
for mino in self.minos:
mino.update()
class Shape:
"""I know the details of a tetromino 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.
def __init__(self, id, color, origin, minos, wall_kick):
self.id = id # Key in dictionary.
self.color = color # Color when drawn.
self.origin = origin # Rotation origin.
self.minos = minos # List of mino coordinates.
self.wall_kick = wall_kick # Wall kick dictionary.
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()))
# The following code only runs the first time the module is imported.
pivot = Vec2(-1, -2)
i_pivot = Vec2(-0.5, -2.5)
o_pivot = Vec2(-0.5, -1.5)
i_minos = (Vec2(-2, -2), Vec2(-1, -2), Vec2(0, -2), Vec2(1, -2))
j_minos = (Vec2(-2, -2), Vec2(-1, -2), Vec2(0, -2), Vec2(-2, -1))
l_minos = (Vec2(-2, -2), Vec2(-1, -2), Vec2(0, -2), Vec2(0, -1))
o_minos = (Vec2(-1, -2), Vec2(0, -2), Vec2(-1, -1), Vec2(0, -1))
s_minos = (Vec2(-2, -2), Vec2(-1, -2), Vec2(-1, -1), Vec2(0, -1))
t_minos = (Vec2(-2, -2), Vec2(-1, -2), Vec2(0, -2), Vec2(-1, -1))
z_minos = (Vec2(-1, -2), Vec2(0, -2), Vec2(-2, -1), Vec2(-1, -1))
# Set up the wall kick info. This is the default wall kick.
kick = (
{
3: (Vec2(-1, 0), Vec2(-1, -1), Vec2(0, 2), Vec2(-1, 2)),
1: (Vec2(1, 0), Vec2(1, -1), Vec2(0, 2), Vec2(1, 2))},
{
0: (Vec2(-1, 0), Vec2(-1, 1), Vec2(0, -2), Vec2(-1, -2)),
2: (Vec2(-1, 0), Vec2(-1, 1), Vec2(0, -2), Vec2(-1, -2))},
{
1: (Vec2(1, 0), Vec2(1, -1), Vec2(0, 2), Vec2(1, 2)),
3: (Vec2(-1, 0), Vec2(-1, -1), Vec2(0, 2), Vec2(-1, 2))},
{
2: (Vec2(1, 0), Vec2(1, 1), Vec2(0, -2), Vec2(1, -2)),
0: (Vec2(1, 0), Vec2(1, 1), Vec2(0, -2), Vec2(1, -2))})
i_kick = (
{
3: (Vec2(1, 0), Vec2(-2, 0), Vec2(1, -2), Vec2(-2, 1)),
1: (Vec2(2, 0), Vec2(-1, 0), Vec2(2, 1), Vec2(-1, -2))},
{
0: (Vec2(-2, 0), Vec2(1, 0), Vec2(-2, -1), Vec2(1, 2)),
2: (Vec2(1, 0), Vec2(-2, 0), Vec2(1, -2), Vec2(-2, 1))},
{
1: (Vec2(-1, 0), Vec2(2, 0), Vec2(-1, 2), Vec2(2, -1)),
3: (Vec2(-2, 0), Vec2(1, 0), Vec2(-2, -1), Vec2(1, 2))},
{
2: (Vec2(2, 0), Vec2(-1, 0), Vec2(2, 1), Vec2(-1, -2)),
0: (Vec2(-1, 0), Vec2(2, 0), Vec2(-1, 2), Vec2(2, -1))})
# Define all the shapes. Adds shapes to Shape dictionary using id as key.
Shape("I", settings.i_color, i_pivot, i_minos, i_kick)
Shape("J", settings.j_color, pivot, j_minos, kick)
Shape("L", settings.l_color, pivot, l_minos, kick)
Shape("O", settings.o_color, o_pivot, o_minos, None)
Shape("S", settings.s_color, pivot, s_minos, kick)
Shape("T", settings.t_color, pivot, t_minos, kick)
Shape("Z", settings.z_color, pivot, z_minos, kick) Namely, at line 52 the Tetromino class accepts an offset value and then uses the shift and update methods to reposition itself.
Then I use pyglet's 2d Euclidean vectors right away to store the relevant data.
Ah, also, at line 155, None is passed to the O shape instead of the default kick data. That's because technically the O shape is not going to use it anyway since the rotation doesn't impact its position.
Posts: 6,803
Threads: 20
Joined: Feb 2020
Sep-18-2023, 05:10 PM
(This post was last modified: Sep-18-2023, 05:10 PM by deanhystad.)
Interesting stuff. A few comments.
I think there is a problem with rotation. Currently the Tetronimo rotation attribute is not used for anything and an angle argument is passed directly to the minos. But I thought the rotation quadrant info was used for the wall kick. And where is the wall kick info used? That logic probably belongs in the Tetronimo rotate method.
Don't repeat code if you can avoid it. This is bad:
i_minos = (Vec2(-2, -2), Vec2(-1, -2), Vec2(0, -2), Vec2(1, -2))
j_minos = (Vec2(-2, -2), Vec2(-1, -2), Vec2(0, -2), Vec2(-2, -1)) This is better:
i_minos = ((-2, -2), (-1, -2), (0, -2), (1, -2))
j_minos = ((-2, -2), (-1, -2), (0, -2), (-2, -1)) Move the vec2() call to the Shape __init__() method.
self.minos = [Vec2(*mino) for mino in minos] Same goes with the rotation center and the kick.
Are there different sized Mino? If not, make cell_size a class variable. Why are you passing batch to Tetrnonimo __init__? Are you batching multiple Tetronimo, or do you want to batch the Mino in a Tetronimo? If the latter, have Tetronimo.__init__() create the batch and pass it to the Mino.__init__.
Since many shapes use the same rotation origin and wall kick, maybe the Shape class should have default values for these. That way you only need to provide values when a shape doesn't use the default values.
It looks like Mino needs to know about rotation origin, but Tetronimo does not. I would remove pivot from the Mino rotate method arguments and make it an attribute of the Mino object.
I would modify the code to look more like this:
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 = [Vec2(*k) for k in 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 = 50
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))
def shift(self, vector):
self.grid_position += vector
def update(self):
self.x = self.grid_position.x * self.width
self.y = self.grid_position.y * self.height
class Tetronimo:
"""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):
"""Create a random Tetronimo."""
return cls(Shape.random(), location)
@classmethod
def get(cls, shape_id, location, rotation=0):
"""Create Tetronimo using shape with matching id."""
return cls(Shape.get(shape_id), location)
def __init__(self, shape, location):
self.shape = shape
self.batch = pyglet.graphics.Batch()
self.minos = [
Mino(shape.color, position, self.batch, shape.pivot) for position in shape.minos
]
self.location = location
def rotate(self, angle):
"""Rotate tetronimo."""
for mino in self.minos:
mino.rotate(self.location, angle)
def shift(self, vector):
"""Move in x and y direction."""
self.location += vector
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)))
Posts: 6
Threads: 1
Joined: Sep 2023
Hold on, it's probably better to post the whole code structure at this point.
This is main.py:
import pyglet
import settings
from colored_window import ColoredWindow
from fixed_resolution import FixedResolution
from tetris import Tetris
class App:
caption = 'Tetris'
fps = 60
bgcolor = settings.bgcolor
def __init__(self):
self.window = ColoredWindow(
400, 800, caption=self.caption, color=self.bgcolor, resizable=True)
self.fixed_res = FixedResolution(self.window, width=100, height=200)
self.window.push_handlers(self)
self.batch = pyglet.graphics.Batch()
self.tetris = Tetris(self.batch)
self.window.push_handlers(self.tetris.key_state_handler)
def on_draw(self):
self.window.clear()
with self.fixed_res:
self.batch.draw()
def update(self, dt):
self.tetris.update(dt)
def run(self):
pyglet.clock.schedule_interval(self.update, 1/self.fps)
pyglet.app.run(1/self.fps)
if __name__ == '__main__':
app = App()
app.run() As you can see, this is where the batch is created.
Quote:[The batch] manages a collection of drawables for batched rendering.
Many drawable pyglet objects accept an optional Batch argument in their constructors. By giving a Batch to multiple objects, you can tell pyglet that you expect to draw all of these objects at once, so it can optimise its use of OpenGL. Hence, drawing a Batch is often much faster than drawing each contained drawable separately.
https://pyglet.readthedocs.io/en/latest/...index.html
I can create multiple batches, but I think that would defeat the purpose. Plus, I'd have to track all the different batches to draw them here... I don't think that's viable.
However, I have to drag this batch object to each drawable object.
Next is tetris.py:
import pyglet
from pyglet.math import Vec2
from field import Field
from input_handler import InputHandler
from tetromino import Tetromino
class Tetris:
field_width = 10
field_height = 20
def __init__(self, batch):
self.batch = batch
self.key_state_handler = pyglet.window.key.KeyStateHandler()
self.input_handler = InputHandler(self.key_state_handler)
self.field = Field(self.field_width, self.field_height)
self.new_tetromino()
def new_tetromino(self):
position = Vec2(self.field_width // 2, self.field_height)
self.tetromino = Tetromino.random(position, self.batch)
def update(self, dt):
self.input_handler.handle_movement_input(self.tetromino, dt)
self.input_handler.handle_rotation_input(self.tetromino)
self.tetromino.update() Here we have the field, from field.py:
from pyglet.math import Vec2
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)] Tetromino, from tetromino.py (I used your last code snippet with just a few changes to make it work with the batch from the main module):
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 = [Vec2(*k) for k in 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))
def shift(self, vector):
self.pivot += vector
self.grid_position += vector
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.shape = shape
self.minos = [
Mino(position + location, shape.pivot + location, shape.color, batch) for position in shape.minos
]
self.location = location
def rotate(self, angle):
"""Rotate tetronimo."""
for mino in self.minos:
mino.rotate(angle)
def shift(self, vector):
"""Move in x and y direction."""
self.location += vector
for mino in self.minos:
mino.shift(vector)
def update(self):
for mino in self.minos:
mino.update()
# 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))) And, finally, my infamous InputHandler, from input_handler.py (I'm posting it only because I'm not using my real name, but to be fair I was in a frenzy to implement the delayed-repeated input and only when I finished I realized the monstrosity of what I came up with):
from pyglet.math import Vec2
from pyglet.window import key
class InputHandler:
def __init__(self, key_state_handler, 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_state_handler
# 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_movement_input(self, tetromino, dt, delay=0.05, 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:
tetromino.shift(Vec2(-1, 0))
self.lx_repeat_timer = 0
else:
self.lx_repeat_timer += dt
else:
self.lx_delay_timer += dt
else:
self.lx_was_pressed = True
tetromino.shift(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:
tetromino.shift(Vec2(1, 0))
self.rx_repeat_timer = 0
else:
self.rx_repeat_timer += dt
else:
self.rx_delay_timer += dt
else:
self.rx_was_pressed = True
tetromino.shift(Vec2(1, 0))
else:
self.rx_was_pressed = False
self.rx_delay_timer = 0
self.rx_repeat_timer = 0
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:
tetromino.shift(Vec2(0, -1))
self.soft_drop_repeat_timer = 0
else:
self.soft_drop_repeat_timer += dt
else:
self.soft_drop_delay_timer += dt
else:
self.soft_drop_was_pressed = True
tetromino.shift(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_input(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
tetromino.rotate(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
tetromino.rotate(-90)
else:
self.rotate_cw_was_pressed = False (Sep-18-2023, 05:10 PM)deanhystad Wrote: I think there is a problem with rotation. Currently the Tetronimo rotation attribute is not used for anything and an angle argument is passed directly to the minos. But I thought the rotation quadrant info was used for the wall kick. And where is the wall kick info used? That logic probably belongs in the Tetronimo rotate method.
Yeah, I'm trying to figure out how to let the Field and Tetromino objects interact with each other. The thing is both the rotate and the shift methods need to know whether the desired movement is allowed or not. Now, the easy way out is to pass the whole Field object to the Tetromino objects. That way the Tetromino object can do pretty much everything with the Field. But, ideally, shouldn't objects operate independently from one another? Isn't that the whole OOP shtick?
Posts: 6,803
Threads: 20
Joined: Feb 2020
Sep-19-2023, 06:20 PM
(This post was last modified: Sep-19-2023, 06:20 PM by deanhystad.)
Object oriented programming has become a catchphrase used to describe any good programming practice. OOP is about using objects to organize the data and functions in your software. Minimizing interdependencies is just a good idea regardless of what paradigm you use.
A mino needs to know about the Field. That is where it ends up. That is who it asks if there is an open space. When the tetronimo comes to rest in the field, the minos move from being a tetronimo to being part of the field. Tetronimos really don't need to know about the field. A tetronimo can ask it's minos if a shift or rotation results in a collision.
Logically I think the minos are divided into two groups. I think each time a tetronimo is created, the minos in the tetronimo are a tetronimo batch. When the minos comes to rest the tetronimo is destroyed, and the minos become part of the field batch. Can you add a batch to a batch or move rectangles from one batch to another?
The dependency tree is not too bad. Game needs to know about Field and Tetronimo. Tetronimo needs to know about Shape and Mino. Mino needs to know about Field. Field needs to know about Mino. It's not often there is only one circular dependency to resolve, and it is a weak dependency. The only thing field needs to know about Mino is how to add them to a batch. To Field, a Mino is nothing more than a Rectangle. Might be that the field doesn't have to know about Mino at all. Whan the tetronimo lands, the minos add rectangles to the field (pass grid coordinates) and destroy themselves.
My thoughts:
Field is a grid of cells. Each cell can be empty or contain a rectangle. The grid provides a method that returns the content of the corresponding cell, a method that creates a rectangle in the specified cell, a method that return the coordinates of a cell, and a method or attribute that Mino can use to make an appropriate rectangle (cell_size attribute?).
The game creates the field which is initially empty.
The game creates a random Tetronimo. The Tetronimo creates multiple Minos. When a Minos is created it creates a rectangle. The rectangle is not added to the grid, but rather floats above the grid.
The game requests the Tetronimo move. The tetronimo tries a move and checks the minos for collisions. If there is a collision, and the move was a rotation, the tetronimo can try a wall kick.
The tetronimo returns a status indicating if the move was successful. If the move was not successful, the game destroys the tetronimo. When the tetronimo is destroyed, it returns to it's last successful location and destroys all the minos. When a minos is destroyed, it tells the field to create a rectangle in the mino's grid location.
|