Nuestra pieza L

“Cuatris”, o nuestro clon de Tetris en Python: ¡comenzamos!

Tetris es un juego que marcó una época. Llegó a occidente en 1986 y fue desarrollado en 1984 por Alekséi Pázhitnov (Muscú, 1956), con la ayuda de Dmitri Pavlovski y Vadim Gerasimov mientras trabajaba en la Academia de Ciencias de la URSS.

La mecánica es sencilla: tenemos una serie de piezas llamadas Tretriminos J, L, Z, S, I, O y T, que caen desde la parte de arriba de la pantalla con ayuda de la gravedad.

Los Tetriminos: I, J, L, O, S, T y Z
Los Tetriminos: I, J, L, O, S, T y Z
  • La pantalla visible del Tetris mide 10 cuadros de ancho por 20 de alto.
  • El jugador, con ayuda de los controles, puede desplazarlas en horizontal (izquierda y derecha) y girarlas de 90 en 90º tanto en sentido horario como en sentido antihorario.
  • Cuando la haya colocado correctamente, podrá precipitarla hacia abajo para encajarlas en las piezas ya depositadas, para formar líneas.
  • Cuando se forma una línea horizontal, todos los segmentos de las piezas que forman una línea desaparecen, y todos los elementos que hubiera por encima, caen.
  • Cada cierto número de líneas, que forman un nivel, la velocidad aumenta.

Lo que vamos a hacer es programar poco a poco un juego como éste, con una estructura que a día de hoy seguirá más o menos este guión:

  1. Dibujar una pieza (básica) y programar la rotación: el artículo de hoy
  2. Programar la gravedad y el resto de movimientos
  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

Para poder aprovechar estos artículos conviene que hayas hecho los anteriores artículos de la serie de Pygame con Python, y la propia serie de Python.

¡Vamos a ello!

Dibujar una pieza

Para este apartado voy a generalizar el ejercicio que hicimos con los cuadrados en los dos artículos anteriores, de tal forma que partiremos de una matriz donde expresaremos con un “1” aquellas posiciones donde hay que pintar un cuadrado, y con un “0” aquellas posiciones donde no hay que pintarlo.

Nuestra pieza, entonces, va a tener los siguientes datos:

  • Una matriz de posiciones que la va a describir
  • Un color para sus cuadros
  • Un color de fondo
  • Una referencia a la pantalla, para poder pintarse
  • El lado del cuadrado que formará parte de la pieza, en píxels

A lo largo de todo el juego voy a manejar dos tipos de coordenadas diferentes: coordenadas en píxels y coordenadas de malla. Las coordenadas de malla se refieren a los cuadrados que forman las piezas en sí, es decir, a cada una de las posiciones de la pantalla de 10×20.

Empezaremos programando una clase con estos datos, y que sea capaz de calcular sus dimensiones “en cuadros” a partir de una matriz, dejando el resto de los métodos vacíos, es decir, con la sentencia pass  de Python.

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
        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'
        pass
        
    def rotarM90(self):
        'Rota la pieza en el sentido antihorario'
        pass

    def __pinta_cuadrado(self, x, y, color):
        'dibuja un cuadrado en las coordenadas de malla especificadas'
        pass

    def __borra_rectangulo(self, rectangulo):
        'borra el área especificada'
        pass

    def pinta(self, x_ini, y_ini):
        'pinta la pieza especificada a partir de las coordenadas de malla x_ini, y_ini'
        pass
                    
    def borra(self, x_ini, y_ini):
        'borra la pieza especificada a partir de las coordenadas de malla x_ini, y_ini'
        pass

Los métodos pinta  y borra funcionarán de la siguiente forma:

  • pinta recorrerá la matriz de definición de la pieza y cuando encuentren un cuadrado lo pintarán en la pantalla usando las función privada (no visible desde fuera de la clase) __pinta_cuadrado
  • borra, por su parte, borrará el área de la pieza completa puesto que ya no es necesario implementar un comportamiento tan selectivo. Usará  __borra_cuadrado para encapsular ahí todo el código que acceda a la pantalla de Pygame.

Este es el código de estas funciones:

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'
        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)

Fíjate en él; no es muy complicado habiendo repasado el artículo donde empezamos a pintar y mover objetos por la pantalla. Lo único destacable, quizá, es que el cuadrado lo pintamos doblemente, para tener un cuadrado más pequeño y más brillante dentro de otro mayor.

Vamos a probarlo con la pieza L. La pieza la definiremos con una matriz, que no es otra cosa que una lista de tuplas, en donde colocaremos un “1” donde queramos pintar un cuadrado, y un “0” donde no. Así:

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

De tal forma que, para crear nuestra clase y dibujarla en una pantalla de trabajo (de momento, de 8 por 8 cuadros), os propongo el siguiente programa de pruebas:

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

# pantalla provisional
ANCHO = 8*SLOT
ALTO = 8*SLOT

x_ini = 2
y_ini = 2

# 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()

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

He resaltado las líneas interesantes; el resto es el clásico bucle de control en Pygame, que poco debería sorprendernos a estas alturas. Copia la clase anterior y este programa en un nuevo fichero en IDLE, y asegúrate de comenzarlo con la sentencia import pygame . Grábalo con un nombre y pulsa F5. Deberías ver una ventana como ésta:

Nuestra pieza L
Nuestra pieza L

Vamos ahora a girarla

Rotación de la pieza

Para esto simplemente tenemos que:

  • Programar la rotación de la matriz de la pieza, rellenando los métodos que ya tenemos preparados.
  • Asociar dichos métodos a la pulsación de dos teclas.
  • La rotación implicará:
    • Detectar la tecla
    • Borrar la pieza
    • Rotar la matriz de la pieza
    • Volver a pintar la pìeza

La rotación es algo que Python pone bastante fácil con una función predefinida que es algo complicada de entender: zip. Esta función recibe como parámetro elementos sobre los que se puede iterar (como son las tuplas) y te devuelve tuplas construirdas a base de asociar el primer elemento de la primera con el primer elemento de la segunda, el primero de la tercera,… y así sucesivamente. En matemáticas, esto sirve en gran medida para hacer operaciones como trasponer matrices.

Con un ejemplo se ve más claro. Introduce estas líneas en el intérprete de IDLE:

matriz_L = [(1, 0), (1, 0), (1, 1)]
matriz_L2 = list(zip(*matriz_L))
matriz_L2

Como he dicho, zip  recibe elementos sobre los que iterar, directamente: no listas. Por eso, no podemos pasarle la matriz de la pieza L directamente ya que es una lista: debemos pasarle el contenido de la lista:*matriz_L . De igual manera, zip devuelve tuplas “sueltas”, por lo que debemos usar la función list  para acabar teniendo una matriz.

Al ejecutar el código de arriba tendremos lo siguiente:

De una L de pie a una J tumbada
De una L de pie a una J tumbada

Hemos pasado de una L de pie a una J “tumbada”, es decir, esa operación es como si hubiéramos colocado un espejo inclinado 45º a nuestra L. Sólo nos queda reflejar la lista de tuplas, es decir, invertir su orden. Esto se puede hacer elegantemente con las construcciones abreviadas que nos permiten recorrer listas. Escribe ahora matriz_L2 así: matriz_L2 = list(zip(*matriz_L[::-1])). Lo que estaremos haciendo será invertir el orden de la lista de tuplas antes de aplicarle zip. Esto nos dejará una L, ahora sí, rotada a favor de las agujas del reloj:

rotado-90
Una L rotada 90º a favor de las agujas del reloj

Para conseguir la rotación contraria, deberíamos aplicar la reflexión una vez ejecutada la función list: matriz_L2 = list(zip(*matriz_L))[::-1].

Rotación contraria
Rotación contraria

Antes de lanzaros a codificar las funciones de rotación, ten en cuenta que ahora la matriz es diferente y sus dimensiones han cambiado. Por lo tanto, habrá que recalcularlas si pretendemos que las funciones de pintado y borrado de las piezas funcionen bien. Dicho lo cual, el código para nuestras funciones es el siguiente:

    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()

Ya sólo queda programar la rotación en el propio programa de pruebas, para lo cual ya os dejo el código del ejercicio completo, convenientemente resaltado:

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
        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'
        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)


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

# pantalla provisional
ANCHO = 8*SLOT
ALTO = 8*SLOT

x_ini = 2
y_ini = 2

# 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()

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:
 
            # Movimiento
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_z:
                    L.borra(x_ini, y_ini)
                    L.rotarM90()
                    L.pinta(x_ini, y_ini)

                if event.key == pygame.K_x:
                    L.borra(x_ini, y_ini)
                    L.rotar90()
                    L.pinta(x_ini, y_ini)

            pygame.display.update()

Si ahora lo ejecutas, al pulsar las teclas X y Z podrás girar la pieza como en este Vine que acabo de cargar en mi cuenta (y que si quieres puedes seguir):


Espero que os haya resultado interesante, y que al mismo tiempo la serie sobre la que iremos progresando hasta llegar a tener el juego completo os resulte motivadora. Estaremos yendo paso a paso e intercalados a los artículos que os proponía como el índice (o el camino) hacia el resultado final iremos viendo todo aquello que nos haga falta, como podrá ser la representación de texto en Pygame, entre otras cosas.

Como siempre, no dudéis en dejarme vuestros comentarios en este artículo o a través del formulario de contacto.

3 comentarios en ““Cuatris”, o nuestro clon de Tetris en Python: ¡comenzamos!

Deja un comentario

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