Cuatris v0.9: nuestro clon de Tetris está casi listo

Poco nos queda ya para tener un clon de Tetris con el que poder jugar con nuestros amigos y familiares. Lo que vamos a hacer en este artículo, quizá cambiando un poco la estructura de la serie, es mostrar un recuadro con la siguiente pieza que saldrá, la puntuación del jugador (en nº de líneas), el nivel en el que está y cuántas líneas le quedan para pasar al siguiente nivel.

El Cuatris: casi, casi terminado
El Cuatris: casi, casi terminado

Recapitulando, hasta ahora hemos hecho los siguientes avances:

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

Para el siguiente capítulo lo que haremos será aplicar una actividad llamada refactorización de forma que tengamos el videojuego 100 % orientado a objetos y sea muy fácil implementar una opción para repetir la partida si el jugador pierde, dándolo así por terminado y dejándoos a vosotros el resto de mejoras que se os ocurran.

Os recomiendo qué debéis leer si llegáis a PItando en este momento, para poder aprovechar este artículo:

  1. La serie de Python, en general.
  2. La serie de Pygame, en particular.

Recuadro de siguiente pieza

El enfoque para poner este recuadro es bien sencillo: dibujaremos otra Pieza más (o un Recipiente adicional, si queremos), con una matriz de 6 por 6 posiciones y bordes grises. Dentro de esa matriz dibujaremos la pieza siguiente.

matriz_siguiente = [[1, 1, 1, 1, 1, 1],
                    [1, 0, 0, 0, 0, 1],
                    [1, 0, 0, 0, 0, 1],
                    [1, 0, 0, 0, 0, 1],
                    [1, 0, 0, 0, 0, 1],
                    [1, 1, 1, 1, 1, 1]]

siguiente = Recipiente (matriz_siguiente, pantalla, SLOT)
siguiente.pinta(13, 0)

Para poder alojar la matriz, deberemos proporcionar más espacio en la pantalla.

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

Necesitaremos por lo tanto, siempre actualizadas, dos objetos de tipo Pieza: la pieza en curso que sufre el movimiento por gravedad y que reacciona a los eventos del juego, y la pieza siguiente que estará estática en nuestro recuadro de “siguiente pieza”. Cuando incorporemos la pieza en movimiento al recipiente, “sacaremos la siguiente pieza del recuadro” y generaremos una nueva, con la función que hemos diseñado.

def siguiente_pieza():
    n_pieza = random.randint(2, 8)
    pieza = Pieza (matrices[n_pieza], pantalla, SLOT)
    return pieza

Esta función tiene unos ligeros cambios puesto que concentraremos todas las sentencias de representación de piezas en el bucle principal de eventos, como veréis al final del artículo en el código completo.

Escribir texto en Pygame

Para escribir textos en Pygame tenemos que tener en cuenta las siguientes cuestiones:

  • Hay que escoger una de las fuentes que tenga el sistema, y un tamaño
  • Debemos acceder directamente a una de las superficies que definimos en el juego y combinar el texto con el fondo de dicha superficie, usando para ello los rectángulos contenedores.

Así, por ejemplo, para escribir el texto “Hola, mundo” en la ventana de Pygame escribiremos lo que sigue (puedes probarlo en IDLE):

import pygame
pygame.init()
basicfont = pygame.font.SysFont(None, 48) 
pantalla = pygame.display.set_mode((480, 320))
pantalla.fill((0,0,0))
text = basicfont.render('JUEGO TERMINADO', True, (0,200,200), (127,127,127))
textrect = text.get_rect()
textrect.centerx= pantalla.get_rect().centerx
textrect.centery= pantalla.get_rect().centery
pantalla.blit(text, textrect)
pygame.display.update()

Este código da como resultado la siguiente ventana:

Representación de texto en Python con Pygame
Representación de texto en Python con Pygame

Lo que hemos hecho en él ha sido:

  • Incializar Pygame,
  • definir la fuente que usaremos (la de por defecto, sin estilos y a 48 puntos de tamaño),
  • preparar la pantalla,
  • crear un gráfico de texto, con color azul y fondo gris
  • y combinar el rectángulo contenedor del texto con la pantalla.

Hay que tener en cuenta que el módulo de texto no interpreta los caracteres de retorno de carro / nueva línea: por lo tanto, si queremos escribir varias líneas de texto, deberemos crear varios objetos con basicfont.render .

Para crear el marcador, que alojaremos bajo el recuadro de la siguiente pieza que hemos discutido en el apartado anterior, dispondremos la siguiente función:

def marcador():
    basicfont = pygame.font.SysFont(None, 48) # a refactorizar
    text = basicfont.render('Líneas: '+str(lineas), True, BLANCO, NEGRO)
    textrect = text.get_rect()
    textrect.left= 13*SLOT
    textrect.top= 8*SLOT
    pantalla.blit(text, textrect)
    text2 = basicfont.render('Nivel: '+str(nivel), True, GRIS, NEGRO)
    textrect2 = text2.get_rect()
    textrect2.left= 13*SLOT
    textrect2.top= 9*SLOT
    pantalla.blit(text2, textrect2)
    text3 = basicfont.render('Siguiente: ' + str(lineas_siguiente_nivel), True, GRIS, NEGRO)
    textrect3 = text3.get_rect()
    textrect3.left= 13*SLOT
    textrect3.top= 10*SLOT 
    pantalla.blit(text3, textrect3)

Además, y para cuando las piezas lleguen al borde superior del recipiente, programaremos otra función que detenga el juego, enseñando un cartel con el mensaje “JUEGO TERMINADO“:

def juego_terminado():
    basicfont = pygame.font.SysFont(None, 48) # a refactorizar
    text = basicfont.render('JUEGO TERMINADO', True, ROJO, GRIS)
    textrect = text.get_rect()
    textrect.centerx= pantalla.get_rect().centerx
    textrect.centery= pantalla.get_rect().centery
    pantalla.blit(text, textrect)

Puntuaciones y niveles

Para ello programaremos contadores de líneas y niveles. Podemos hacer, por ejemplo, que cada 20 líneas la velocidad de la gravedad aumente (reduciendo t_GRAVEDAD) e indicaremos este hecho incrementando el nivel. Os recomiendo que no incrementéis la velocidad más allá de un punto en el que el juego sería injugable; por ejemplo, podéis dejar de reducir t_GRAVEDAD cuando éste sea menor a 100 ó 200 milisegundos.

Mejorando el bucle principal del juego

Ya sólo nos queda atar cabos gracias a estas nuevas funciones y objetos en el juego, realizando cambios en el bucle principal del programa. Puedes consultar la solución completa y los cambios en el bucle (si no te salen) expandiendo este bloque de código:

import pygame
import random

class Pieza:
    'Modela una pieza de Cuatris :)'
    def __init__(self, matriz, 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, colores[0], 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] != 0:
                    self.__pinta_cuadrado(x_ini+j, y_ini+i, colores[self.matriz[i][j]])
                    
    def borra(self, x_ini, y_ini):
        'borra 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] != 0:
                    self.__pinta_cuadrado(x_ini+j, y_ini+i, colores[0])

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

    def colisiona_con(self, resto):
        for i in range(0,len(self.matriz)):
            for j in range(0,len(self.matriz[i])):
                if self.matriz[i][j] != 0:
                    if resto.matriz[self.y + i][self.x + j] != 0:
                        return True
        return False

class Recipiente(Pieza):
    
    def __quitar_fila(self, linea):
        'retira la fila indicada, poniendo una nueva fila en la parte de arriba de la pantalla'
        for fila in range(linea, 0, -1):
            self.matriz[fila] = self.matriz[fila - 1]
        self.matriz[0] = [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
        
    def comprobar_lineas(self, otra_pieza):
        'comprueba las líneas formadas por la nueva pieza'
        lineas = 0
        for fila in range(otra_pieza.y, otra_pieza.y + otra_pieza.filas):
            producto = 1;
            for valor in self.matriz[fila]:
                producto = producto * valor
                if producto == 0:
                    break
            if producto > 0:
                lineas = lineas +1
                self.__quitar_fila(fila)
        return lineas
        
    def incorporar(self, otra_pieza):
        'incorpora la matriz de otra pieza a la de si mismo, devolviendo el nº de líneas que se han hecho'
        for i in range(0,len(otra_pieza.matriz)):
            for j in range(0,len(otra_pieza.matriz[i])):
                if otra_pieza.matriz[i][j] != 0:
                    self.matriz[otra_pieza.y+i][otra_pieza.x+j] = otra_pieza.matriz[i][j]
        return self.comprobar_lineas(otra_pieza)      

def siguiente_pieza():
    n_pieza = random.randint(2, 8)
    pieza = Pieza (matrices[n_pieza], pantalla, SLOT)
    return pieza

def marcador():
    basicfont = pygame.font.SysFont(None, 48) # a refactorizar
    text = basicfont.render('Líneas: '+str(lineas), True, BLANCO, NEGRO)
    textrect = text.get_rect()
    textrect.left= 13*SLOT
    textrect.top= 8*SLOT
    pantalla.blit(text, textrect)
    text2 = basicfont.render('Nivel: '+str(nivel), True, GRIS, NEGRO)
    textrect2 = text2.get_rect()
    textrect2.left= 13*SLOT
    textrect2.top= 9*SLOT
    pantalla.blit(text2, textrect2)
    text3 = basicfont.render('Siguiente: ' + str(lineas_siguiente_nivel), True, GRIS, NEGRO)
    textrect3 = text3.get_rect()
    textrect3.left= 13*SLOT
    textrect3.top= 10*SLOT 
    pantalla.blit(text3, textrect3)

def juego_terminado():
    basicfont = pygame.font.SysFont(None, 48) # a refactorizar
    text = basicfont.render('JUEGO TERMINADO', True, ROJO, GRIS)
    textrect = text.get_rect()
    textrect.centerx= pantalla.get_rect().centerx
    textrect.centery= pantalla.get_rect().centery
    pantalla.blit(text, textrect)
    

BLANCO = (255,255,255)
GRIS = (127,127,127)
NEGRO = (0, 0, 0)
MAGENTA = (200,0,200)
VERDE = (0, 200, 0)
ROJO = (200, 0, 0)
AZUL = (0, 0, 200)
AMARILLO = (200, 200, 0)
MARRON = (0, 200, 200)
BLANCO = (200, 200, 200)
FPS = 60
SLOT = 40

colores = [NEGRO, GRIS, MARRON, AZUL, BLANCO, VERDE, ROJO, MAGENTA, AMARILLO]

# pantalla 21 x 22 y posición inicial de la pieza
ANCHO = 21*SLOT
ALTO = 22*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_Recipiente = [[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]

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

matriz_J = [(0, 3),
            (0, 3),
            (3, 3)]

matriz_I = [(4, 0),
            (4, 0),
            (4, 0),
            (4, 0)]

matriz_S = [(0, 5, 5),
            (5, 5, 0)]

matriz_Z = [(6, 6, 0),
            (0, 6, 6)]

matriz_T = [(7, 7, 7),
            (0, 7, 0)]

matriz_O = [(8, 8),
            (8, 8)]

matrices = [0, matriz_Recipiente, matriz_L, matriz_J, matriz_I, matriz_S, matriz_Z, matriz_T, matriz_O]

matriz_siguiente = [[1, 1, 1, 1, 1, 1],
                    [1, 0, 0, 0, 0, 1],
                    [1, 0, 0, 0, 0, 1],
                    [1, 0, 0, 0, 0, 1],
                    [1, 0, 0, 0, 0, 1],
                    [1, 1, 1, 1, 1, 1]]

siguiente = Recipiente (matriz_siguiente, pantalla, SLOT)
siguiente.pinta(13, 0)

recipiente = Recipiente (matriz_Recipiente, pantalla, SLOT)
recipiente.pinta(0, 0)

pieza = siguiente_pieza()
pieza.pinta(4,0)
pieza_siguiente = siguiente_pieza()
pieza_siguiente.pinta(15,1)

pygame.display.update()

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

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

continuar = True

lineas = 0
lineas_siguiente_nivel = 20
nivel = 0

marcador()

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:
                pieza.auto_borra()
                pieza.mover(0, 1)
                if pieza.colisiona_con(recipiente):
                    pygame.time.set_timer(GRAVEDAD, t_GRAVEDAD)
                    pieza.mover(0,-1)
                    # L.auto_pinta()
                    # Incorporar pieza a "P"
                    recipiente.auto_borra()
                    lineas = lineas + recipiente.incorporar(pieza)
                    # niveles
                    if lineas >= lineas_siguiente_nivel:
                        nivel = nivel +1
                        lineas_siguiente_nivel = lineas_siguiente_nivel + 20
                        print("NIVEL:",nivel)
                        if t_GRAVEDAD > 100:
                            t_GRAVEDAD = t_GRAVEDAD - 1000
                    marcador()
                    recipiente.auto_pinta()
                    pieza_siguiente.auto_borra()
                    pieza = pieza_siguiente
                    pieza.pinta(4,0)
                    pieza_siguiente = siguiente_pieza()
                    pieza_siguiente.pinta(15,1)
                    if pieza.colisiona_con(recipiente):
                        continuar = False
                        juego_terminado()
                else:
                    pieza.auto_pinta()
        
            if event.type == pygame.KEYDOWN:

                # El giro de las piezas puede provocar colisiones por el lado derecho
                if event.key == pygame.K_z:
                    pieza.auto_borra()
                    pieza.rotarM90()
                    if pieza.colisiona_con(recipiente):
                        pieza.mover(-1,0)
                    pieza.auto_pinta()

                if event.key == pygame.K_x:
                    pieza.auto_borra()
                    pieza.rotar90()
                    if pieza.colisiona_con(recipiente):
                        pieza.mover(-1,0)
                    pieza.auto_pinta()

                if event.key == pygame.K_LEFT:
                    pieza.auto_borra()
                    pieza.mover(-1,0)
                    if pieza.colisiona_con(recipiente):
                        # deshacer
                        pieza.mover(1, 0)
                    pieza.auto_pinta()

                if event.key == pygame.K_RIGHT:
                    pieza.auto_borra()
                    pieza.mover(1,0)
                    if pieza.colisiona_con(recipiente):
                        # deshacer
                        pieza.mover(-1, 0)
                    pieza.auto_pinta()

                # acelerar gravedad
                if event.key == pygame.K_DOWN:
                    pygame.time.set_timer(GRAVEDAD, t_CAIDA)
                else:
                    pygame.time.set_timer(GRAVEDAD, t_GRAVEDAD)

            pygame.display.update()

Siguientes (y últimos) pasos

El juego no es continuo, es decir, no ofrece la opción de empezar una nueva partida cuando perdemos. También hay código repetido y, además, el programa no está totalmente orientado a objetos.

Lo que haremos en la siguiente entrega será hablar del refinado del código, o la refactorización de programas, y, como ejemplo, convertiremos nuestro juego en un programa totalmente orientado a objetos y que, además:

  • No tenga código Python replicado
  • Tenga un punto único de reinicialización
  • Sea fácilmente configurable para, por ejemplo, variar el tamaño global de la ventana.

Un comentario en “Cuatris v0.9: nuestro clon de Tetris está casi listo

Deja un comentario

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