Cap. 17 - PyGame

PyGame es un paquete que no es parte de la distribución estándar de Python, así que si aun no lo tienes instalado (caso en que un intento de hacer import pygame fallará), puedes descargar e instalar una versión actualizada desde PyGame - Download.

Pygame viene con un conjunto muy amplio de tutoriales, ejemplos y archivos de ayuda, por lo cual hay mucho para explorar y experimentar con esta clase.

17.1) El loop del juego

La estructura de los juegos que consideraremos seguirá siempre el siguiente patrón:

En todo juego, en la sección setup creamos una ventana, leemos y preparamos cierto contenido, y entonces entramos en el loop del juego (game loop). El loop del juego hace continuamente cuatro cosas principales:

import pygame

def main():
    """ Set up del juego e inicio de su loop principal """
    pygame.init()             # Prepara el módulo pygame para su uso
    tamaño_superficie = 480   # Tamaño deseado de la superficie, en pixels.

    # Crear superficie (ancho, alto) y su ventana.
    superficie_principal = pygame.display.set_mode((tamaño_superficie, tamaño_superficie))

    # Setear los datos necesarios para describir un pequeño rectángulo y su color
    pequeño_rectangulo = (300, 200, 150, 90)
    un_color = (255, 0, 0)        # Un color es una combinación de (Rojo, Verde, Azul)

    while True:
        ev = pygame.event.poll()    # Sondeo a ver si ocurrió un evento
        if ev.type == pygame.QUIT:  # Si el evento es que se hizo click en el botón cerrar ventana
            break                   #   ... abandonar el loop principal del juego

        # Actualiza los objetos de tu juego y la estructura de datos aquí...

        # Dibujamos todo desde cero en cada frame.
        # Así que comencemos por llenar todo con el color de fondo
        superficie_principal.fill((0, 200, 255))

        # Pintemos un pequeño rectángulo en la superficie principal
        superficie_principal.fill(un_color, pequeño_rectangulo)

        # Ahora que el dibujo está pronto, le decimos a pygame que lo muestre!
        pygame.display.flip()

    pygame.quit()     # Si hemos salido del loop principal del juego, sólo queda cerrar la ventana.

main()

Este programa muestra una ventana que se queda ahí hasta que la cerramos:

Pygame hace todos sus dibujos sobre superficies rectangulares. Tras inicializar PyGame en la línea 5, creamos una ventana que contenga a nuestra superficie principal (líneas 6-9). El loop principal del juego se extiende desde la línea 15 a la 30, con los siguientes aspectos fundamentales a tener en cuenta:

17.2) Mostrar imágenes y texto

Para dibujar una imagen en la superficie principal, cargamos la imagen (digamos, una pelota de playa) en una superficie nueva de su propiedad.

Así que en la sección setup, antes de entrar en el game loop, cargaríamos la imagen, así:

pelota = pygame.image.load("pelota.png")

superficie_principal.blit(pelota, (100, 120))

Para mostrar texto, debemos hacer 3 cosas. Antes de entrar al loop, instanciamos un objeto font:

# Instanciar fuente Courier de 16 puntos para dibujar texto.
mi_fuente = pygame.font.SysFont('Courier', 16)

Y después de la lìnea 28, una vez màs, utilizamos el mètodo render de font para crear una nueva superficie que contiene los píxeles del texto dibujado, y luego, como en el caso de las imágenes, hacemos blit de la nueva superficie en la superficie principal.

el_texto = mi_fuente.render('Hola, mundo!', True, (0,0,0))
superficie_principal.blit(el_texto, (10, 10))

Demostraremos estas dos nuevas funcionalidades contando los frames - iteraciones del loop principal - y llevando la cuenta del tiempo.

import pygame
import time

def main():
    pygame.init()             # Prepara el módulo pygame para su uso
    superficie_principal = pygame.display.set_mode((480, 240))

    # Leer una imagen para dibujar. Usa una de tu propiedad.
    # PyGame maneja los tipos de imagen típicos: gif, jpg, png, etc.
    pelota = pygame.image.load("pelota.png")

    # Crear una fuenta para mostrar el texto
    mi_fuente = pygame.font.SysFont('Courier', 16)

    cuenta_frames = 0
    frecuencia_frames = 0
    t0 = time.time()

    while True:
        # Sondeo por un evento del teclado, mouse, joystick, etc.
        ev = pygame.event.poll()
        if ev.type == pygame.QUIT:  # Si el evento es que se hizo click en el botón cerrar ventana
            break                   #   ... abandonar el loop principal del juego

        # Implementamos aquí un poco de la lógica del juego
        cuenta_frames += 1
        if cuenta_frames % 500 == 0:
            t1 = time.time()
            frecuencia_frames = 500 / (t1-t0)
            t0 = t1

        # Completamente redibujamos la superficie, comenzando por el fondo
        superficie_principal.fill((0, 200, 255))

        # Ponemos un rectángulo rojo en algún punto de la superficie
        superficie_principal.fill((255, 0, 0), (300, 100, 150, 90))

        # Copiamos nuestra imagen a la superficie, en la posición (x, y) señalada:
        superficie_principal.blit(pelota, (100, 120))

        # Creamos una nueva superficie con una imagen del texto
        el_texto = mi_fuente.render('Frame = {0},  frecuencia = {1:.2f} fps'
                  .format(cuenta_frames, frecuencia_frames), True, (0,0,0))
        # Copiamos la superficie del texto a la superficie principal
        superficie_principal.blit(el_texto, (10, 10))

        # Ahora que el dibujo está pronto, le decimos a pygame que lo muestre!
        pygame.display.flip()

    pygame.quit()     # Si hemos salido del loop principal del juego, sólo queda cerrar la ventana.

main()

La frecuencia de frames es ridículamente alta - mucho más alta de lo que el ojo humano puede procesar.

17.3) Dibujando un tablero para el problema de las N reinas

Ya habíamos resuelto el problema de las N reinas. Para el caso del tablero 8x8, una de las soluciones era [6, 4, 2, 0, 5, 7, 1, 3].

Crearemos un nuevo módulo para el código de este proyecto, que llamaremos dibujar_reinas.py.

Comencemos con un fondo de cuadrados rojos y negros para el tablero.

import pygame

def dibujar_tablero(tablero):
    """ Dibuja un tablero de ajedrez con reinas, a partir de tablero. """

    pygame.init()
    colores = [(255,0,0), (0,0,0)]    # Establece dos colores [rojo, negro]

    n = len(tablero)                         # Este es un tablero de ajedrez NxN
    tamaño_superficie = 480                  # Superficie física disponible
    tamaño_cuadro = tamaño_superficie // n   # Tamaño de un cuadro
    tamaño_superficie = n * tamaño_cuadro    # Ajuste para que la proporción sea exacta.

    # Crear la superficie de (ancho, alto), con su ventana
    superficie = pygame.display.set_mode((tamaño_superficie, tamaño_superficie))

Aquí hemos precomputado tamaño_cuadro, el tamaño de cada cuadro dado como un número entero, para que los cuadrados encajen exactamente dentro del ancho de pantalla.

Ahora dibujemos los cuadrados, en el loop del juego. Necesitamos un loop anidado: el loop externo recorrerá las filas del tablero, el interno las columnas.

    # Dibujar un tablero de ajedrez vacío como fondo
    for fila in range(n):           # Dibujar cada fila en el tablero.
        color_indice = fila % 2     # Cambiar el color inicial de cada fila
        for columna in range(n):    # Recorrer las columnas dibujando cuadrados
            este_cuadro = (columna*tamaño_cuadro, fila*tamaño_cuadro, tamaño_cuadro, tamaño_cuadro)
            superficie.fill(colores[color_indice], este_cuadro)
            # ahora invertimos el índice de color para el próximo cuadro
            color_indice = (color_indice + 1) % 2

Hay dos ideas importantes en este código:

Para ver el tablero propiamente dicho, hay que agregar el código necesario para hacer el flip de la imagen al display, y también el sondeo de eventos para detectar al menos si el usuario decide cerrar la ventana:

    while True:
        # Sondeo por un evento del teclado, mouse, joystick, etc.
        ev = pygame.event.poll()
        if ev.type == pygame.QUIT:  # Si el evento es que se hizo click en el botón cerrar ventana
            break                   #   ... abandonar el loop principal del juego    
        
        (...)		# Líneas del loop principal, ya mostradas arriba
        
        # Ahora que el dibujo está pronto, le decimos a pygame que lo muestre!
            pygame.display.flip()

Un simple llamado a la función dibujar_tablero() nos permite ver el resultado, que es un tablero de ajedrez cuyo tamaño depende del largo de la lista pasada como argumento:

Ahora hay que dibujar las reinas! Recordemos que nuestra solución [6, 4, 2, 0, 5, 7, 1, 3] significa que en la columna 0 del tablero queremos una reina en la fila 6, en la columna 1 queremos una reina en la fila 4, y así sucesivamente. Por lo tanto necesitamos un loop que recorra cada reina de la solución:

for (columna, fila) in enumerate(tablero):
    # dibujar una reina en columna, fila...

En este capítulo ya habíamos utilizado la imagen de una bola, así que la volveremos a usar en lugar de una reina. En el código setup, antes del loop principal del game, cargamos la imagen de la pelota (como habíamos hecho antes) y en el cuerpo del loop agregamos la línea:

superficie.blit(pelota, (columna * tamaño_cuadro, fila * tamaño_cuadro))

Estamos cerca, pero nos faltaría centrar a esas reinas en sus cuadros!

El offset que necesitamos viene dado por la fórmula: (tamaño_cuadro - tamaño_pelota)/2

offset_pelota = (tamaño_cuadro - pelota.get_width()) // 2

Ahora sólo falta retocar el código que dibuja a la pelota y terminamos:

superficie.blit(pelota, (columna * tamaño_cuadro + offset_pelota, fila * tamaño_cuadro + offset_pelota))

¿Qué pasaría si la pelota fuera mayor que el cuadro? En este caso, offset_pelota sería negativo y la pelota seguiría estando centrada en el cuadrado, sólo que se saldría de sus bordes o incluso podría taparlo.

Aquí está el programa completo:

import pygame

def dibujar_tablero(tablero):
    """ Dibuja un tablero de ajedrez con reinas, a partir de tablero. """

    pygame.init()
    colores = [(255,0,0), (0,0,0)]    # Establece dos colores [rojo, negro]

    n = len(tablero)                         # Este es un tablero de ajedrez NxN
    tamaño_superficie = 480                  # Superficie física disponible
    tamaño_cuadro = tamaño_superficie // n   # Tamaño de un cuadro
    tamaño_superficie = n * tamaño_cuadro    # Ajuste para que la proporción sea exacta.

    # Crear la superficie de (ancho, alto), con su ventana
    superficie = pygame.display.set_mode((tamaño_superficie, tamaño_superficie))

    # Leer la imagen de la pelota (que vamos a usar para representar a las reinas)
    pelota = pygame.image.load("pelota.png")

    # Definir el offset de la pelota (para centrarla en lo cuadros)
    offset_pelota = (tamaño_cuadro - pelota.get_width()) // 2

    while True:
        # Sondeo por un evento del teclado, mouse, joystick, etc.
        ev = pygame.event.poll()
        if ev.type == pygame.QUIT:  # Si el evento es que se hizo click en el botón cerrar ventana
            break                   #   ... abandonar el loop principal del juego

        # Dibujar un tablero de ajedrez vacío como fondo
        for fila in range(n):           # Dibujar cada fila en el tablero.
            color_indice = fila % 2     # Cambiar el color inicial de cada fila
            for columna in range(n):    # Recorrer las columnas dibujando cuadrados
                este_cuadro = (columna*tamaño_cuadro, fila*tamaño_cuadro, tamaño_cuadro, tamaño_cuadro)
                superficie.fill(colores[color_indice], este_cuadro)
                # ahora invertimos el índice de color para el próximo cuadro
                color_indice = (color_indice + 1) % 2

        # Dibujamos las reinas
        for (columna, fila) in enumerate(tablero):
            superficie.blit(pelota, (columna * tamaño_cuadro + offset_pelota, fila * tamaño_cuadro + offset_pelota))

        # Ahora que el dibujo está pronto, le decimos a pygame que lo muestre!
        pygame.display.flip()

    pygame.quit()     # Si hemos salido del loop principal del juego, sólo queda cerrar la ventana.

if __name__ == '__main__':
    dibujar_tablero([0, 5, 3, 1, 6, 4, 2])    # 7 x 7 para testear el ajuste del tamaño de ventana
    dibujar_tablero([6, 4, 2, 0, 5, 7, 1, 3])
    dibujar_tablero([9, 6, 0, 3, 10, 7, 2, 4, 12, 8, 11, 5, 1])  # 13 x 13
    dibujar_tablero([11, 4, 8, 12, 2, 7, 3, 15, 0, 14, 10, 6, 13, 1, 5, 9])

Aquí vale la pena detenerse en un punto. La condición if __name __ al final del programa se fija si el nombre del programa que estamos ejecutando es __main__. Esto permite distinguir entre el caso en que este módulo es ejecutado como el programa principal, y el caso en que es importado por otro módulo.

En la sección Problema de 8 reinas, parte 2 nuestro programa principal se veía así:

def main():
    import random
    rng = random.Random()   # Instanciar al generador

    tablero = list(range(8))     # Generate la permutación inicial
    encontrados = 0
    intentos = 0
    while encontrados < 10:
       rng.shuffle(tablero)
       intentos += 1
       if not tiene_conflictos(tablero):
           print("Encontré la solución {0} en {1} intentos.".format(tablero, intentos))
           intentos = 0
           encontrados += 1

main()

Ahora sólo necesitamos dos cambios.

dibujar_reinas.dibujar_tablero(tablero)

Y con esto llegamos a una versión muy satisfactoria del programa que es capaz de buscar soluciones al problema de las N reinas, y cuando encuentra cada una, muestra el tablero de ajedrez con las reinas en las posiciones dadas por la misma.

17.4) Sprites

Un sprite es un objeto que se puede mover en un juego, y tiene estado interno y comportamiento propios. Por ejemplo, los personajes que se mueven por su cuenta en juegos de video son sprites, las naves o vehículos son sprites, las balas y bombas son sprites, y el propio jugador es un sprite.

La programación orientada a objetos (POO) es la forma ideal de describir esta situación: cada objeto puede tener sus propios atributos y estado interno, y un par de métodos. Hagamos algunos experimentos con nuestro tablero de N reinas. En vez de ubicar a la reina en su posición final, la vamos a soltar en lo alto del tablero, y dejar que ella caiga a su posición, tal vez incluso rebotando en el camino.

La primera encapsulación que vamos a necesitar es convertir a cada reina en un objeto. Vamos a tener una lista de todos los sprites activos (es decir una lista de los objetos reina) y considerar un par de cosas más en nuestro loop principal del juego:

Comenzaremos con un objeto simple, sin movimiento ni animación: sólo vamos a implementar el andamiaje necesario para ver cómo encajan todas las piezas:

class SpriteReina:

    def __init__(self, img, posicion_final):
        """ Crear e inicializar una reina para la
            posicino_final dada en el tablero
        """
        self.imagen = img
        self.posicion_final = posicion_final
        self.posicion = posicion_final

    def update(self):
        return                # No hacemos nada por el momento.

    def dibujar(self, superficie):
        superficie.blit(self.imagen, self.posicion)

Le hemos dado al sprite 3 atributos: una imagen para dibujar, una posición final y su posición actual.

En este código por ahora no hemos hecho nada en el método update y nuestro método dibujar (que probablemente seguirá siendo así de simple en el futuro) simplemente se dibuja a sí mismo en la posición actual en la superficie que le es pasada como parámetro.

Con su definición de clase pronta, podemos ahora instanciar nuestras N reinas, ponerlas en una lista de sprites, y hacer que el loop del juego llame a los métodos update y dibujar en cada frame. Los nuevos trozos de código junto con el loop del juego modificados se ven así:

(...)		#La parte inicial del código no cambia

    lista_sprites = []     # Mantenemos una lista con todos los sprites del juego

    #Creamos un objeto sprite para cada reina y lo ponemos en la lista.
    for (columna, fila) in enumerate(tablero):
        una_reina = SpriteReina(pelota,
                (columna*tamaño_cuadro + offset_pelota, fila*tamaño_cuadro + offset_pelota))
        lista_sprites.append(una_reina)

    while True:
        # Sondeo por un evento del teclado, mouse, joystick, etc.
        ev = pygame.event.poll()
        if ev.type == pygame.QUIT:  # Si el evento es que se hizo click en el botón cerrar ventana
            break                   #   ... abandonar el loop principal del juego

        #Pedirle a cada sprite que se actualice
        for sprite in lista_sprites:
            sprite.update()

        # Dibujar un tablero de ajedrez vacío como fondo
        (...)		#Esta parte del código no cambia

        # Pedirle a cada sprite que se dibuje a sí mismo
        for sprite in lista_sprites:
            sprite.dibujar(superficie)

        # Ahora que el dibujo está pronto, le decimos a pygame que lo muestre!
        pygame.display.flip()

(...)		#La parte final del código no cambia

Esto funciona igual que nuestra versión anterior, pero hemos dejado ya todo pronto para agregar modificaciones con facilidad en los próximos pasos.

Comencemos con una reina que cae. En todo momento tendrá una velocidad en cierta dirección (en este caso será la dirección y porque se trata de una caída vertical). Así que en el método update del objeto queremos cambiar su posición vertical de acuerdo a su velocidad. Si nuestro tablero de N reinas está flotando en el espacio, la velocidad se mantendría constante, pero como está en la Tierra, tenemos gravedad, la cual cambiará la velocidad en cada intervalo, así que queremos una pelota que cae cada vez más rápido. La gravedad es la misma para todas las reinas, así que no la haremos parte de cada objeto, sino que la mantendremos como una variable a nivel del módulo. Y haremos otro cambio: comenzaremos con cada reina en lo alto del tablero, de forma que pueda caer hacia su posición final. Con estos cambios, obtenemos ahora lo siguiente:

class SpriteReina:

    def __init__(self, img, posicion_final):
        """ Crear e inicializar una reina para la
            posicino_final dada en el tablero
        """
        self.imagen = img
        self.posicion_final = posicion_final
        (x, y) = posicion_final
        self.posicion = (x, 0)      # Comenzamos en lo alto, justo sobre posicion_final
        self.velocidad_y = 0        # La velocidad inicial de caída es 0

    def update(self):
        self.velocidad_y += gravedad;             # La gravedad incrementa la velocidad
        (x, y) = self.posicion
        nueva_posicion_y = y + self.velocidad_y   # La velocidad mueve la bola
        self.posicion = (x, nueva_posicion_y)     #    a su nueva posicion

    def dibujar(self, superficie):             # Igual que antes (nada cambió)
        superficie.blit(self.imagen, self.posicion)

Tenemos ahora un nuevo tablero en que cada reina comienza en el tope de su columna y cae cada vez más rápido, hasta que alcanza el piso del tablero y desaparece para siempre.

El próximo paso es hacer que la bola rebote cuando alcance su propia posicion_final.

Una forma realista de simular el rebote de un objeto es hacer que pierda cierta cantidad de energía en cada golpe.

Sólo necesitamos un par de cambios en el método update, que ahora queda así:

    def update(self):
        self.velocidad_y += gravedad;             # La gravedad incrementa la velocidad
        (x, y) = self.posicion
        nueva_posicion_y = y + self.velocidad_y   # La velocidad mueve la bola
        (x_final, y_final) = self.posicion_final
        distancia_faltante = y_final - nueva_posicion_y  #Qué tan lejos estamos del "piso"

        if (distancia_faltante < 0):            # Ya estamos por debajo del "piso"?
            self.velocidad_y = -0.65 * self.velocidad_y     # Rebotar
            nueva_posicion_y = y_final + distancia_faltante # Moverse arriba del "piso"

        self.posicion = (x, nueva_posicion_y)     # Establecemos nuestra nueva posición

Pruébalo por ti mismo y juega con el parámetro gravedad para ver caídas más o menos rápidas de las reinas.

17.5) Eventos

El único tipo de evento que hemos manejado hasta ahora ha sido el evento QUIT. Pero también podríamos detectar eventos keydown y keyup (respectivamente, evento apretar y soltar tecla), movimiento del mouse, botón del mouse up o down, etc. Puedes consultar la documentación de PyGame, siguiendo el link a Event.

Cuando tu programa sondea por eventos y recibe de PyGame un objeto evento, el tipo del evento determinará qué información secundaria está disponible.

Por ejemplo:

El objeto evento cuyo tipo es NOEVENT se retorna si no hay eventos esperando. Los eventos pueden imprimirse, lo que permite experimentar y jugar con ellos. Por lo tanto, si ponemos las siguientes líneas de código en el loop del juego, justo después de haber hecho el sondeo de eventos, tendremos mucha información disponible sobre eventos:

        if ev.type != pygame.NOEVENT:   # Sólo imprimir si hay algo interesante!
            print(ev)

Si ahora presionas la barra espaciadora mientras ves el tablero del juego, verás el reporte del evento correspondiente. Haz 3 clicks en tu mouse. Mueve el mouse en la ventana. (Esto causará una gran cascada de eventos, así que puede que necesites también filtrar estos para que no se impriman). Obtendrás un output que se verá así:

<Event(4352-AudioDeviceAdded {'which': 0, 'iscapture': 0})>
<Event(4352-AudioDeviceAdded {'which': 0, 'iscapture': 1})>
<Event(32768-ActiveEvent {})>
<Event(32774-WindowShown {'window': None})>
<Event(32768-ActiveEvent {'gain': 1, 'state': 1})>
<Event(32785-WindowFocusGained {'window': None})>
<Event(770-TextEditing {'text': '', 'start': 0, 'length': 0, 'window': None})>
<Event(32768-ActiveEvent {'gain': 1, 'state': 0})>
<Event(32783-WindowEnter {'window': None})>
<Event(1024-MouseMotion {'pos': (291, 81), 'rel': (0, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(32770-VideoExpose {})>
<Event(32776-WindowExposed {'window': None})>
<Event(768-KeyDown {'unicode': ' ', 'key': 32, 'mod': 0, 'scancode': 44, 'window': None})>
<Event(771-TextInput {'text': ' ', 'window': None})>
<Event(769-KeyUp {'unicode': ' ', 'key': 32, 'mod': 0, 'scancode': 44, 'window': None})>
<Event(1024-MouseMotion {'pos': (291, 82), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (291, 83), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (291, 84), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (291, 86), 'rel': (0, 2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (291, 87), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (291, 88), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (291, 89), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (291, 90), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (291, 91), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (291, 92), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (290, 92), 'rel': (-1, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (290, 93), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (290, 94), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (289, 94), 'rel': (-1, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1025-MouseButtonDown {'pos': (289, 94), 'button': 1, 'touch': False, 'window': None})>
<Event(1026-MouseButtonUp {'pos': (289, 94), 'button': 1, 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (289, 95), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (289, 98), 'rel': (0, 3), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (289, 99), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (289, 101), 'rel': (0, 2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (289, 102), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (289, 103), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (289, 104), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (289, 105), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (290, 106), 'rel': (1, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (290, 107), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (290, 108), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (290, 109), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (290, 110), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (291, 110), 'rel': (1, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (291, 111), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (292, 111), 'rel': (1, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1025-MouseButtonDown {'pos': (292, 111), 'button': 1, 'touch': False, 'window': None})>
<Event(1026-MouseButtonUp {'pos': (292, 111), 'button': 1, 'touch': False, 'window': None})>
<Event(1025-MouseButtonDown {'pos': (292, 111), 'button': 1, 'touch': False, 'window': None})>
<Event(1026-MouseButtonUp {'pos': (292, 111), 'button': 1, 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (292, 113), 'rel': (0, 2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (293, 116), 'rel': (1, 3), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (293, 118), 'rel': (0, 2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (293, 119), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (293, 121), 'rel': (0, 2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (293, 124), 'rel': (0, 3), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (293, 128), 'rel': (0, 4), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (291, 135), 'rel': (-2, 7), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (286, 151), 'rel': (-5, 16), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (283, 165), 'rel': (-3, 14), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (277, 177), 'rel': (-6, 12), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (276, 180), 'rel': (-1, 3), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (272, 184), 'rel': (-4, 4), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (262, 193), 'rel': (-10, 9), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (245, 203), 'rel': (-17, 10), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (220, 213), 'rel': (-25, 10), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (193, 217), 'rel': (-27, 4), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (138, 219), 'rel': (-55, 2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (124, 214), 'rel': (-14, -5), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (113, 209), 'rel': (-11, -5), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (105, 202), 'rel': (-8, -7), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (97, 194), 'rel': (-8, -8), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (88, 181), 'rel': (-9, -13), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (84, 168), 'rel': (-4, -13), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (79, 154), 'rel': (-5, -14), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (78, 148), 'rel': (-1, -6), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (78, 143), 'rel': (0, -5), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (78, 141), 'rel': (0, -2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (78, 139), 'rel': (0, -2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (78, 138), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (79, 136), 'rel': (1, -2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (80, 134), 'rel': (1, -2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (95, 124), 'rel': (15, -10), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (132, 103), 'rel': (37, -21), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (179, 88), 'rel': (47, -15), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (224, 76), 'rel': (45, -12), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (272, 71), 'rel': (48, -5), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (312, 71), 'rel': (40, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (359, 74), 'rel': (47, 3), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (394, 82), 'rel': (35, 8), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (425, 94), 'rel': (31, 12), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (455, 111), 'rel': (30, 17), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (455, 113), 'rel': (0, 2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (456, 115), 'rel': (1, 2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (457, 116), 'rel': (1, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (457, 118), 'rel': (0, 2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (457, 119), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (457, 121), 'rel': (0, 2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (457, 124), 'rel': (0, 3), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (456, 128), 'rel': (-1, 4), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (450, 143), 'rel': (-6, 15), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (442, 160), 'rel': (-8, 17), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (434, 176), 'rel': (-8, 16), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (428, 187), 'rel': (-6, 11), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (421, 197), 'rel': (-7, 10), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (419, 200), 'rel': (-2, 3), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (415, 203), 'rel': (-4, 3), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (412, 205), 'rel': (-3, 2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (408, 206), 'rel': (-4, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (404, 207), 'rel': (-4, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (391, 207), 'rel': (-13, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (379, 205), 'rel': (-12, -2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (366, 200), 'rel': (-13, -5), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (355, 194), 'rel': (-11, -6), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (345, 185), 'rel': (-10, -9), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (335, 174), 'rel': (-10, -11), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (327, 163), 'rel': (-8, -11), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (322, 156), 'rel': (-5, -7), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (320, 153), 'rel': (-2, -3), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (319, 149), 'rel': (-1, -4), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (318, 147), 'rel': (-1, -2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (317, 145), 'rel': (-1, -2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (317, 144), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (317, 143), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (317, 142), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (317, 141), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (317, 139), 'rel': (0, -2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (317, 138), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (324, 131), 'rel': (7, -7), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (337, 119), 'rel': (13, -12), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (354, 108), 'rel': (17, -11), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (364, 100), 'rel': (10, -8), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (366, 99), 'rel': (2, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (367, 98), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (369, 97), 'rel': (2, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (370, 97), 'rel': (1, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (371, 97), 'rel': (1, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (372, 96), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (373, 96), 'rel': (1, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (374, 96), 'rel': (1, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (375, 95), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (377, 95), 'rel': (2, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (379, 95), 'rel': (2, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (381, 95), 'rel': (2, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (382, 95), 'rel': (1, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (383, 95), 'rel': (1, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (388, 84), 'rel': (5, -11), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (396, 67), 'rel': (8, -17), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (402, 52), 'rel': (6, -15), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (404, 49), 'rel': (2, -3), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (404, 47), 'rel': (0, -2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (405, 46), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (405, 45), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (406, 44), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (406, 43), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (406, 42), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (407, 42), 'rel': (1, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (407, 41), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (408, 40), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (409, 39), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (410, 38), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (410, 37), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (411, 36), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (411, 35), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (412, 35), 'rel': (1, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (412, 34), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (413, 33), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (414, 32), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (414, 31), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (415, 30), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (416, 29), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (417, 27), 'rel': (1, -2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (418, 26), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (420, 25), 'rel': (2, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (421, 24), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (422, 22), 'rel': (1, -2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (423, 21), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (424, 20), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (425, 19), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (426, 18), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (429, 14), 'rel': (3, -4), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (430, 13), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (430, 12), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (431, 11), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (431, 10), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (432, 9), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (433, 8), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (433, 7), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (434, 6), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (435, 4), 'rel': (1, -2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (436, 3), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (437, 2), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (437, 1), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (438, 0), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(1024-MouseMotion {'pos': (438, 0), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>
<Event(32768-ActiveEvent {'gain': 0, 'state': 0})>
<Event(32784-WindowLeave {'window': None})>
<Event(32787-WindowClose {'window': None})>

Así que hagamos ahora estos cambios a nuestro código en la parte del código que responde a los eventos:

    while True:
        # Sondeo por un evento del teclado, mouse, joystick, etc.
        ev = pygame.event.poll()
        if ev.type == pygame.QUIT:  # Si el evento es que se hizo click en el botón cerrar ventana
            break                   #   ... abandonar el loop principal del juego

        if ev.type == pygame.KEYDOWN:
            key = ev.dict['key']
            if key == 27:                  # Si es la tecla Escape ...
                break                      #   abandonar el juego.
            if key == ord('r'):
                colores[0] = (255, 0, 0)    # Cambiar a rojo + negro.
            elif key == ord('g'):
                colores[0] = (0, 255, 0)    # Cambiar a verde (green) + negro.
            elif key == ord('b'):
                colores[0] = (0, 0, 255)    # Cambiar a azul (blue) + negro.

        if ev.type == pygame.MOUSEBUTTONDOWN: # Se presionó el botón del mouse?
            pos_del_click = ev.dict['pos']    # Obtener las coordenadas.
            print(pos_del_click)              # Imprimirlas.

Las líneas centrales muestran un manejo típico del evento KEYDOWN - si una tecla fue presionada, nos fijamos qué tecla es, y ejecutamos cierta acción.

Las líneas que responden al evento MOUSEBUTTONDOWN no hacen casi nada: se limitan a imprimir las coordenadas del mouse en el momento del click.

Como un ejercicio final de esta sección, escribiremos una mejor versión de nuestro manejador de los clicks del mouse.

Comenzaremos con código que encuentra cuál es el sprite que se encuentra en la posición cliqueada (o si no hay ninguno). Agregamos el método contiene_punto a la clase SpriteReina, que devuelve True si el punto está dentro del rectángulo del sprite:

    def contiene_punto(self, pt):
        """ Retorna True si mi rectángulo de sprite contiene al punto pt """
        (mi_x, mi_y) = self.posicion
        mi_ancho = self.imagen.get_width()
        mi_alto = self.imagen.get_height()
        (x, y) = pt
        return ( x >= mi_x and x < mi_x + mi_ancho and
               y >= mi_y and y < mi_y + mi_alto)

Ahora en el loop del juego, una vez que hemos visto el evento del mouse, determinamos qué reina, si es que hay alguna, debería ser la que responda al evento:

        if ev.type == pygame.MOUSEBUTTONDOWN:
            pos_del_click = ev.dict['pos']
            for sprite in lista_sprites:
                if sprite.contiene_punto(pos_del_click):
                    sprite.manejar_click()
                    break

Para terminar, debemos escribir un método manejar_click en la clase SpriteReina. Cuando se cliquea un sprite, agregaremos un poco de velocidad hacia arriba, es decir, pateamos la bola hacia arriba.

    def manejar_click(self):
        self.velocidad_y += -0.3   # Patearla para arriba

Con estos cambios tenemos un juego que se puede jugar! Prueba a ver si puedes mantener todas las pelotas en el aire, no dejando que ninguna toque el suelo!

17.6) Una ola de animaciones

Muchos juegos tienen sprites animados: se agachan, saltan o disparan. ¿Cómo lo hacen?

Considera esta secuencia de 10 imágenes. Si las mostramos en rápida sucesión, Duke nos saludará. (Duke es un amistoso personaje del reino de Javalandia).

Una imagen compuesta que contiene pequeños parches (patches) a ser utilizados como parte de una animación se llama sprite sheet (hoja de sprites).

El sprite sheet fue preparado cuidadosamente: cada uno de los 10 parches está espaciado a 50 píxeles de su vecino. Por lo tanto, si quisiéramos dibujar el parche número 4 (comenzando a contar desde 0), dibujaríamos el rectángulo que comienza en la posición x = 200 y tiene 50 píxeles de ancho, dentro del sprite sheet.

El método blit que hemos venido usando - para copiar píxeles desde una superficie a otra - puede copiar un sub-rectángulo de la superficie fuente. Por lo tanto, la gran idea aquí es que cada vez que vayamos a dibujar a Duke, no vamos a hacer blit del sprite sheet completo, sino que pasaremos un argumento extra, correspondiente a un rectángulo, para indicar cuál es la porción del spread sheet que queremos utilizar.

Agregaremos en esta sección más código al que ya tenemos para dibujar las reinas. Lo que queremos es poner algunas instancias de Duke en algunas partes del tablero. Si el usuario hace click en una de ellas, responderemos saludando, un ciclo completo de su animación.

Pero antes de hacerlo, necesitamos otro cambio. Hasta ahora, nuestro loop del juego ha estado corriendo con un frame rate altísimo, bastante fluctuante e impredecible. Por lo cual elegimos algunos números mágicos para la gravedad y los rebotes y patadas a la pelota, más bien por ensayo y error. Si vamos a animar a más sprites, debemos adaptar el loop de nuestro juego para que opere a una frecuencia de frames conocida y estable. Esto nos permitirá planear de mejor modo nuestras animaciones.

PyGame nos da las herramientas para hacer esto en tan sólo dos líneas de código. En la sección setup del juego, instanciamos un nuevo objeto Clock.

    mi_reloj = pygame.time.Clock()

Y justo al final del loop del juego, llamamos a un método de este objeto que limita el frame rate a lo que nosotros especifiquemos.

        mi_reloj.tick(60)  # Dejar pasar el tiempo para que el frame rate se reduzca a 60 fps 

Verás que tienes que ir atrás y ajustar los números para gravedad y pateado de la pelota, cosa de que los movimientos se vean bien con este frame rate que es mucho más lento.

Cuando planeamos una animación para que sólo funcione bien a cierta frecuencia de frames, decimos que hemos horneado la animación.

Para seguir con la política que ya aplicamos para el dibujo y movimientos de las reinas, queremos crear una clase SpriteDuke que tenga los mismos métodos que la clase SpriteReina. Entonces podremos agregar una o más instancias de Duke a nuestra lista_sprites, y nuestro loop del juego llamará entonces a métodos de la instancia de Duke. Comencemos por un esbozo del esqueleto de la nueva clase:

class SpriteDuke:

    def __init__(self, img, posicion_final):
        self.imagen = img
        self.posicion = posicion_final

    def update(self):
        return

    def dibujar(self, superficie):
        return

    def manejar_click(self):
        return

    def contiene_punto(self, pt):
        # Usar el código de ReinaSprite aquí (es el mismo)
        return

Los únicos cambios que tenemos que hacer a la implementación del juego están todos en la sección setup. Vamos a leer el sprite sheet de Duke e instanciar un par de instancias de la clase SpriteDuke, en las posiciones en que lo queremos ubicar en el tablero. Así que antes de entrar en el loop del juego, agregamos este código:

    # Cargar el sprite sheet de Duke
    duke_sprite_sheet = pygame.image.load("duke_spritesheet.png")

    # Instanciar dos instancias de Duke, y ponerlas en el tablero
    duke1 = SpriteDuke(duke_sprite_sheet,(tamaño_cuadro*2, 0))
    duke2 = SpriteDuke(duke_sprite_sheet,(tamaño_cuadro*5, tamaño_cuadro))

    # Y luego agregarlos a la lista de sprites que es manejada por el loop principal del juego
    lista_sprites.append(duke1)
    lista_sprites.append(duke2)

Ahora el loop del game va a testear si cada instancia fue cliqueada, lo cual llamará al manejador de clicks para cada instancia. También va a llamar a update y draw para todas las instancias. Todos los cambios que nos quedan por hacer han de realizarse dentro de la clase SpriteDuke.

Comencemos por dibujar uno de los parches. Introduciremos un nuevo atributo, parche_actual, en la clase. Guarda un valor entre 0 y 9, y determina cuál parche hay que dibujar.

    def dibujar(self, superficie):
        patch_rectangulo = (self.parche_actual * 50, 0,
                    50, self.imagen.get_height())
        superficie.blit(self.imagen, self.posicion, patch_rectangulo)

    def __init__(self, img, posicion_final):
        self.imagen = img
        self.posicion = posicion_final
        self.parche_actual = 0

Ahora comencemos a implementar la animación en sí. Necesitamos modificar el método update de SpriteDuke para que si estamos en plena animación, se modifique el valor de parche_actual con al frecuencia adecuada, y se decida correctamente cuándo es tiempo de volver a la posición de descanso y detener la animación. Una cuestión importante es que el frame rate del juego no tiene por qué ser el mismo que el de la animación (animation rate), que es la frecuencia con la que vamos cambiando entre los parches de animación de Duke. Planearemos el ciclo de animación de Duke para que dure 1 segundo, por lo cual queremos poder ejecutar 10 cambios de parche cada 60 llamadas a update (porque, recordemos, el frame rate del juego es de 60 frames por segundo). Así es como se va haciendo el horneado de la animación! Así que necesitamos un contador de animación en la clase, que será cero cuando no estemos animando, y que se irá incrementando en cada llamada de update hasta llegar a 59 (durante la animación), volviendo entonces a 0. Podemos entonces dividir el contador de la animación por 6 (división entera), para deducir el valor de parche_actual y dibujar el parche correcto.

    def update(self):
        if self.contador_frame_animacion > 0:
            self.contador_frame_animacion = (self.contador_frame_animacion + 1 ) % 60
            self.parche_actual = self.contador_frame_animacion // 6

    def __init__(self, img, posicion_final):
        self.imagen = img
        self.posicion = posicion_final
        self.parche_actual = 0
        self.contador_frame_animacion = 0

Observar que si contador_frame_animacion es cero, es decir si Duke está quieto, nada ocurre en el update. Pero si inicializamos la cuenta del contador, contará hasta 59 antes de regresar a cero.

Ahora, ¿cómo y cuándo iniciamos la animación? Lo hacemos así, como respuesta a un click del mouse:

    def manejar_click(self):
        if self.contador_frame_animacion == 0:
            self.contador_frame_animacion = 5	

Dos observaciones importantes:

No olvidar inicializar los dos nuevos atributos parche_actual y contador_frame_animacion en el método __init__ (como ya hemos señalado antes). Así queda el código final de la clase:

class SpriteDuke:

    def __init__(self, img, posicion_final):
        self.imagen = img
        self.posicion = posicion_final
        self.parche_actual = 0
        self.contador_frame_animacion = 0

    def update(self):
        if self.contador_frame_animacion > 0:
            self.contador_frame_animacion = (self.contador_frame_animacion + 1 ) % 60
            self.parche_actual = self.contador_frame_animacion // 6

    def dibujar(self, superficie):
        patch_rectangulo = (self.parche_actual * 50, 0,
                    50, self.imagen.get_height())
        superficie.blit(self.imagen, self.posicion, patch_rectangulo)

    def manejar_click(self):
        if self.contador_frame_animacion == 0:
            self.contador_frame_animacion = 5

    def contiene_punto(self, pt):
        """ Retorna True si mi rectángulo de sprite contiene al punto pt """
        (mi_x, mi_y) = self.posicion
        mi_ancho = self.imagen.get_width()
        mi_alto = self.imagen.get_height()
        (x, y) = pt
        return ( x >= mi_x and x < mi_x + mi_ancho and
               y >= mi_y and y < mi_y + mi_alto)

Ahora vemos dos instancias de Duke en el tablero, y si hacemos click sobre alguna de ellas, nos saluda.

17.7) Aliens - un caso de estudio

Encontrar los juegos de ejemplo que vienen en el paquete PyGame y juega al juego Aliens. Luego lee el código en un editor o entorno Python en que se muestren las líneas de código.

Hace unas cuantas cosas más avanzadas que las que hicimos nosotros, y se basa en el framework de PyGame para la mayor parte de su lógica. Aquí hay algunas cuestiones que puedes observar: (hacerlo con tiempo)

17.8) Reflexiones

La programación orientada a objetos es una herramienta muy buena para la organización del software.

17.9) Glosario

17.10) Ejercicios

1) Diviértete con Python y con PyGame.

 

2) Intencionalmente dejamos un bug en el código que animaba a Duke. Si haces click en un cuadro a la derecha de Duke, saludará también. ¿Por qué? Corrígelo con un ajuste que sólo requiera modificar una línea de código.

 

3) Usa tu buscador preferido para encontrar una librería de imágenes para "sprite sheet playing cards". Crea una lista [0..51] para representar las 52 cartas del mazo de cartas inglesas. Mezcla las cartas, toma las 5 primeras como en una partida de poker. Muestra en pantalla tu mano de poker. (hacerlo)

 

4) Como el juego Aliens se desarrolla en el espacio exterior, no hay gravedad: los disparos vuelan para siempre y las bombas no se aceleran al caer. Agrega un poco de gravedad al juego. Decide al hacerlo si permitirás que tus propios tiros podrán caer sobre tu cabeza y matarte en tal caso. (hacerlo - es recomendable crear una copia de aliens.py de PyGame y trabajar sobre esa copia para no alterar el original)

 

5) Esos apestosos Aliens se cruzan entre sí sin dañarse! Modifica el juego para que choquen y se destruyan mutuamente en una grandiosa explosión. (hacerlo)