Aplicando gravedad a nuestro clon de Tetris

En esta semana vamos a retomar Cuatris, nuestro clon de Tetris, donde lo dejamos la pasada semana. Recordando el plan, la secuencia que íbamos a seguir es la siguiente:

  1. Dibujar una pieza (básica) y programar la rotación: lo vimos la semana pasada
  2. Programar la gravedad y el resto de movimientos: el artículo de hoy
  3. Colisiones de la pieza con otras piezas y el borde de la pantalla.
  4. Haciendo líneas y retocando la mecánica anterior
  5. Puntuaciones y niveles
  6. Borde, marcador, tabla de puntuaciones
  7. Refinando los gráficos de las piezas

Lo que haremos para eso va a ser usar los eventos de usuario que proporciona Pygame, de tal modo que nos adecuaremos a las reglas internas de funcionamiento del bucle de procesado de eventos que ya tenemos, programando la ocurrencia de un evento cada cierto tiempo (dos segundos por ahora).

Además, programaremos dos movimientos más, izquierda y derecha, a lo largo de una pantalla que ya redimensionaremos a su tamaño definitivo (10 cuadros de ancho por 20 de alto).

¡Manos a la obra!

(Foto: Museos Científicos Coruñeses)

Eventos de usuario en Pygame

Los eventos en Pygame no son más que códigos numéricos que la librería reconoce, y existen 8 disponibles para el uso libre por parte de los programadores. En concreto, disponemos de todos los valores que se encuentran entre pygame.USEREVENT y pygame.NUMEVENTS:

Eventos de usuario: entre 24 y 32
Eventos de usuario: entre 24 y 32

Los eventos de usuario, entre otras muchas cosas (que iremos viendo a medida que las necesitemos) se pueden temporizar fácilmente, es decir, podemos pedirle a Pygame que produzca un evento cada cierto tiempo. Prueba el siguiente programa:

import pygame
import datetime

pygame.init()

FPS = 60

MI_EVENTO = pygame.USEREVENT + 1
t_MI_EVENTO = 2000
pygame.time.set_timer(MI_EVENTO, t_MI_EVENTO)

clock = pygame.time.Clock()
continuar = True

while continuar:
    clock.tick(FPS)

    for event in pygame.event.get():
        if event.type == MI_EVENTO:
            t = datetime.datetime.now()
            print ("Mi evento en t = ", t.second)

pygame.quit()

Al ejecutarlo, verás que cada 2 segundos el mensaje “Mi evento en t = ” aparece, mostrando los segundos del instante actual. Sal del programa usando la combinación de teclas Control + C. El trozo de código importante está resaltado, entre las líneas 8 y 10. Experimenta con ellas.

Salida del programa
Salida del programa

Lo que haremos en nuestro juego será ampliarlo para que, cada 2 segundos, Pygame nos notifique un evento que llamaremos GRAVEDAD y ante el que moveremos la pieza una posición hacia abajo. Eso exige unos preparativos previos.

Ampliando la pieza

Necesitamos que la pieza amplíe su comportamiento para:

  • Almacenar las posiciones actuales.
  • Sea capaz de borrarse de las posiciones que esté ocupando en la pantalla en un momento dado
  • Podamos invocar un método para que se mueva un determinado número de cuadros en un momento dado.

Veamos una propuesta de clase que lo hace, atendiendo a las líneas resaltadas en el código:

class Pieza:
    'Modela una pieza de Cuatris :)'
    def __init__(self, matriz, color, fondo, pantalla, lado_cuadrado):
        self.matriz = matriz
        self.color = color
        self.fondo = fondo
        self.pantalla = pantalla
        self.lado_cuadrado = lado_cuadrado
        # posición de la pieza
        self.x = 0
        self.y = 0
        self.calcula_dimensiones()

    def calcula_dimensiones(self):
        self.filas = len(self.matriz)
        self.columnas = len(self.matriz[0])
        
    def rotar90(self):
        'Rota la pieza en el sentido horario'
        self.matriz = list(zip(*self.matriz[::-1]))
        self.calcula_dimensiones()
        
    def rotarM90(self):
        'Rota la pieza en el sentido antihorario'
        self.matriz = list(zip(*self.matriz))[::-1]
        self.calcula_dimensiones()

    def __pinta_cuadrado(self, x, y, color):
        'dibuja un cuadrado en las coordenadas de malla especificadas'
        cuadrado = (x*self.lado_cuadrado,
                    y*self.lado_cuadrado,
                    self.lado_cuadrado,
                    self.lado_cuadrado)
        cuadrado_interno = (x*self.lado_cuadrado+8,
                            y*self.lado_cuadrado+8,
                            self.lado_cuadrado-16,
                            self.lado_cuadrado-16)
        pygame.draw.rect(self.pantalla,
                         (int(color[0]/2),
                          int(color[1]/2),
                          int(color[2]/2)),
                         cuadrado,
                         0)
        pygame.draw.rect(self.pantalla, color, cuadrado_interno, 0)

    def __borra_rectangulo(self, rectangulo):
        'borra el área especificada'
        pygame.draw.rect(self.pantalla, self.fondo, rectangulo, 0)

    def pinta(self, x_ini, y_ini):
        'pinta la pieza especificada a partir de las coordenadas de malla x_ini, y_ini'
        # actualizamos la posición de la pieza
        self.x = x_ini
        self.y = y_ini
        for i in range(0,len(self.matriz)):
            for j in range(0,len(self.matriz[i])):
                if self.matriz[i][j] == 1:
                    self.__pinta_cuadrado(x_ini+j, y_ini+i, self.color)
                    
    def borra(self, x_ini, y_ini):
        'borra la pieza especificada a partir de las coordenadas de malla x_ini, y_ini'
        rectangulo_pieza = (x_ini*self.lado_cuadrado,
                          y_ini*self.lado_cuadrado,
                          (x_ini+self.columnas)*self.lado_cuadrado,
                          (y_ini+self.filas)*self.lado_cuadrado)
        self.__borra_rectangulo(rectangulo_pieza)

    # Movimiento de la pieza
    def mover(self, desplazamiento_x, desplazamiento_y):
        'mueve la pieza el desplazamiento indicado (sin pintarla), en coordenadas de malla'
        self.x = self.x + desplazamiento_x
        self.y = self.y + desplazamiento_y

    # Funciones que hacen uso del estado de la pieza
    def auto_pinta(self):
        'pintar la pieza en las propiedades autocontenidas'
        self.pinta(self.x, self.y)

    def auto_borra(self):
        'borrar la piezaden las propiedades autocontenidas'
        self.borra(self.x, self.y)

Lo único que hacemos aquí es inicializar las coordenadas a valores significativos la primera vez que la pintamos en la pantalla. Por otro lado, le hemos dotado de funciones que borran y pintan la pieza utilizando esas coordenadas internas, y otra que la mueve un determinado número de casillas en las direcciones horizontal y vertical.

Programando el evento GRAVEDAD y los movimientos horizontales de la pieza

Este apartado se resuelve simplemente tomando la técnica que vimos en el primer listado del artículo, e incorporándolo al bucle. Posteriormente tendremos que modificar el bucle para que todas las funciones de rotación y movimiento tengan en cuenta las coordenadas que nuestra clase almacena en todo momento, es decir: usar las funciones nuevas.

El código queda así:

BLANCO = (255,255,255)
NEGRO = (0, 0, 0)
MAGENTA = (200,0,200)
FPS = 60
SLOT = 40

# pantalla 10 x 20 y posición inicial de la pieza
ANCHO = 10*SLOT
ALTO = 20*SLOT
x_ini = 4
y_ini = 0

# inicializamos pygame
pygame.init()

# definición de la pantalla
pantalla = pygame.display.set_mode((ANCHO, ALTO))
pantalla.fill(NEGRO)

matriz_L = [(1, 0),
            (1, 0),
            (1, 1)]

L = Pieza (matriz_L, MAGENTA, NEGRO, pantalla, SLOT)
L.pinta(x_ini, y_ini)

pygame.display.update()

# reloj de control de refresco
clock = pygame.time.Clock()

# Evento de gravedad
GRAVEDAD = pygame.USEREVENT + 1
t_GRAVEDAD = 2000 #milisegundos
pygame.time.set_timer(GRAVEDAD, t_GRAVEDAD)

continuar = True
while continuar: 
    # pausa hasta el siguiente "tick" de reloj
    clock.tick(FPS)

    # detección de evento QUIT (aspa)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            continuar = False
        else:
            # Procesar la gravedad
            if event.type == GRAVEDAD:
                L.auto_borra()
                L.mover(0, 1)
                L.auto_pinta()
     
            # Movimiento
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_z:
                    L.auto_borra()
                    L.rotarM90()
                    L.auto_pinta()

                if event.key == pygame.K_x:
                    L.auto_borra()
                    L.rotar90()
                    L.auto_pinta()

                if event.key == pygame.K_LEFT:
                    L.auto_borra()
                    L.mover(-1,0)
                    L.auto_pinta()

                if event.key == pygame.K_RIGHT:
                    L.auto_borra()
                    L.mover(1,0)
                    L.auto_pinta()

            pygame.display.update()

Si lo pruebas, verás que puedes desplazar la pieza en horizontal usando las flechas del cursor, además de girarla en ambos sentidos con las teclas Z y X. Fíjate en que, además, he aprovechado para redimensionar la pantalla y recolocar la pieza en su posición inicial.

En la siguiente entrega viene programaremos los límites de la pantalla en lo que a movimiento se refiere, y empezaremos a apilar piezas en el fondo de la pantalla de tal forma que encajen bien. Hasta entonces no dudes en ponerte en contacto conmigo si tuvieras cualquier duda, tanto usando los comentarios de este artículo como a través del formulario de contacto.

Por si tienes algún problema, te dejo el código fuente completo en este cuadro (que tendrás que desplegar).

import pygame

class Pieza:
    'Modela una pieza de Cuatris :)'
    def __init__(self, matriz, color, fondo, pantalla, lado_cuadrado):
        self.matriz = matriz
        self.color = color
        self.fondo = fondo
        self.pantalla = pantalla
        self.lado_cuadrado = lado_cuadrado
        # posición de la pieza
        self.x = 0
        self.y = 0
        self.calcula_dimensiones()

    def calcula_dimensiones(self):
        self.filas = len(self.matriz)
        self.columnas = len(self.matriz[0])
        
    def rotar90(self):
        'Rota la pieza en el sentido horario'
        self.matriz = list(zip(*self.matriz[::-1]))
        self.calcula_dimensiones()
        
    def rotarM90(self):
        'Rota la pieza en el sentido antihorario'
        self.matriz = list(zip(*self.matriz))[::-1]
        self.calcula_dimensiones()

    def __pinta_cuadrado(self, x, y, color):
        'dibuja un cuadrado en las coordenadas de malla especificadas'
        cuadrado = (x*self.lado_cuadrado,
                    y*self.lado_cuadrado,
                    self.lado_cuadrado,
                    self.lado_cuadrado)
        cuadrado_interno = (x*self.lado_cuadrado+8,
                            y*self.lado_cuadrado+8,
                            self.lado_cuadrado-16,
                            self.lado_cuadrado-16)
        pygame.draw.rect(self.pantalla,
                         (int(color[0]/2),
                          int(color[1]/2),
                          int(color[2]/2)),
                         cuadrado,
                         0)
        pygame.draw.rect(self.pantalla, color, cuadrado_interno, 0)

    def __borra_rectangulo(self, rectangulo):
        'borra el área especificada'
        pygame.draw.rect(self.pantalla, self.fondo, rectangulo, 0)

    def pinta(self, x_ini, y_ini):
        'pinta la pieza especificada a partir de las coordenadas de malla x_ini, y_ini'
        # actualizamos la posición de la pieza
        self.x = x_ini
        self.y = y_ini
        for i in range(0,len(self.matriz)):
            for j in range(0,len(self.matriz[i])):
                if self.matriz[i][j] == 1:
                    self.__pinta_cuadrado(x_ini+j, y_ini+i, self.color)
                    
    def borra(self, x_ini, y_ini):
        'borra la pieza especificada a partir de las coordenadas de malla x_ini, y_ini'
        rectangulo_pieza = (x_ini*self.lado_cuadrado,
                          y_ini*self.lado_cuadrado,
                          (x_ini+self.columnas)*self.lado_cuadrado,
                          (y_ini+self.filas)*self.lado_cuadrado)
        self.__borra_rectangulo(rectangulo_pieza)

    # Movimiento de la pieza
    def mover(self, desplazamiento_x, desplazamiento_y):
        'mueve la pieza el desplazamiento indicado (sin pintarla), en coordenadas de malla'
        self.x = self.x + desplazamiento_x
        self.y = self.y + desplazamiento_y

    # Funciones que hacen uso del estado de la pieza
    def auto_pinta(self):
        'pintar la pieza en las propiedades autocontenidas'
        self.pinta(self.x, self.y)

    def auto_borra(self):
        'borrar la piezaden las propiedades autocontenidas'
        self.borra(self.x, self.y)


BLANCO = (255,255,255)
NEGRO = (0, 0, 0)
MAGENTA = (200,0,200)
FPS = 60
SLOT = 40

# pantalla 10 x 20 y posición inicial de la pieza
ANCHO = 10*SLOT
ALTO = 20*SLOT
x_ini = 4
y_ini = 0

# inicializamos pygame
pygame.init()

# definición de la pantalla
pantalla = pygame.display.set_mode((ANCHO, ALTO))
pantalla.fill(NEGRO)

matriz_L = [(1, 0),
            (1, 0),
            (1, 1)]

L = Pieza (matriz_L, MAGENTA, NEGRO, pantalla, SLOT)
L.pinta(x_ini, y_ini)

pygame.display.update()

# reloj de control de refresco
clock = pygame.time.Clock()

# Evento de gravedad
GRAVEDAD = pygame.USEREVENT + 1
t_GRAVEDAD = 2000 #milisegundos
pygame.time.set_timer(GRAVEDAD, t_GRAVEDAD)

continuar = True
while continuar: 
    # pausa hasta el siguiente "tick" de reloj
    clock.tick(FPS)

    # detección de evento QUIT (aspa)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            continuar = False
        else:
            # Procesar la gravedad
            if event.type == GRAVEDAD:
                L.auto_borra()
                L.mover(0, 1)
                L.auto_pinta()
     
            # Movimiento
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_z:
                    L.auto_borra()
                    L.rotarM90()
                    L.auto_pinta()

                if event.key == pygame.K_x:
                    L.auto_borra()
                    L.rotar90()
                    L.auto_pinta()

                if event.key == pygame.K_LEFT:
                    L.auto_borra()
                    L.mover(-1,0)
                    L.auto_pinta()

                if event.key == pygame.K_RIGHT:
                    L.auto_borra()
                    L.mover(1,0)
                    L.auto_pinta()

            pygame.display.update()

 

2 comentarios en “Aplicando gravedad a nuestro clon de Tetris

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *