Ir al contenido principal

Python + Pygame (VI): Tutorial - Klondike (I)

Como complemento a las entradas en las que voy  compartiendo pequeños desarrollos de juegos que estoy realizando en python con programación orientada a objetos (POO), inicio con éste una serie de posts a modo de tutoriales con objeto de que puedan comprenderse mejor los fuentes de los mismos que se puden descargar de este blog.

En todos estos desarrollos he intentado que los fuentes estén convenientemente comentados, incluso en ocasiones 'pecando' por exceso, pero aún así, tal y como digo, creo que algunas explicaciones adicionales sobre ciertos aspectos pueden ser útiles para aquellas personas que se introduzcan en la programación en python con pygame.

En esta primera entrada, comienzo con la primera entrega del tutorial correspondiente al juego de cartas Klondike (la variante más popular del solitario), que consiste en ir apilando, en orden ascendente, los cuatro palos de una baraja francesa o de póker; y cuando termine con todas las entregas publicaré mi propia versión del mismo.

En este juego se utilizan 52 cartas de una baraja estándar (es decir, todas menos los dos comodines). Todos lo conoceremos porque adquirió una gran popularidad cuando Microsoft lo incluyó en su sistema operativo Windows 3.0.2.​

En la programación de este solitario, y en general de cuaquier juego de cartas, juegan un papel fundamental el ratón y la gestión de sus eventos, ya que hay que implementar, entre otras, la funcionalidad del arrastre de cartas ('drag and drop').

Para ello, pygame permite interactuar con la entrada del ratón en un entorno de juego a modo de interface entre el juego en sí y el jugador, de forma que se pueda detectar la posición del ratón y gestionar los eventos asociados al mismo: clics realizados, arrastrar y soltar objetos ('drag and drop'), etc.

Pues bien, en esta primera entrega de este tutorial voy a empezar por cómo implementar esta última funcionalidad: arrastrar y soltar objetos.

Antes que nada voy a crear, centrar y dibujar la ventana:

!/usr/bin/env python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# TUTORIAL: KLONDIKE(I).
#
# Pygame es una colección de módulos Python diseñados para crear videojuegos.
#
# Autor: Mikel García Larragan.
# https://mikelgarcialarragan.blogspot.com/

import pygame
import os


def klondike():
    while True:
        # inicializar módulos internos de pygame.
        pygame.init()
        # Obtener las dimensiones de la pantalla.
        informacion_pantalla = pygame.display.Info()
        # Establecer tamaño de la ventana.
        ancho_pantalla = informacion_pantalla.current_w - int(informacion_pantalla.current_w * 0.5)
        alto_pantalla = informacion_pantalla.current_h - int(informacion_pantalla.current_h * 0.45)
        # crear una superficie de visualización: la ventana, y centrarla.
        os.environ['SDL_VIDEO_CENTERED']='1'
        ventana = pygame.display.set_mode((ancho_pantalla,alto_pantalla))
        # Título de la ventana.
        pygame.display.set_caption('Klondike')
        # Icono de la ventana.
        # Cargar la imagen del icono.
        icono = pygame.image.load('klondike.ico')
        # Establecer el icono de la ventana en tiempo de ejecución.
        pygame.display.set_icon(icono)
        # Cargar la imagen del fondo del juego.
        fondo_juego = pygame.image.load('fondo_juego.png')
        # Cambiar el formato de píxel del fondo del juego para crear una copia que se dibujará más rápidamente en la pantalla.
        fondo_juego = fondo_juego.convert()
        # Cargar archivos de fuentes y sonidos.
        sonido_click = pygame.mixer.Sound('click.wav')
        # Crear canal para sonidos.
        canal1 = pygame.mixer.Channel(0)
        # Reloj para ejecutar el juego.
        reloj = pygame.time.Clock()
        # El juego se ejecutará a 60 frames por segundo.
        fps = 60
        # Bucle juego.
        while True:
            # Ejecutar el siguiente frame.
            reloj.tick(fps)
            # Escuchar cada uno de eventos de la lista de eventos de Pygame.
            for evento in pygame.event.get():
                # Finalizar el juego cuando se produzca el evento de terminación del programa (cierre de la ventana).
                if evento.type == pygame.QUIT:
                    pygame.quit()
                    return
            # Copiar la imagen de fondo de la ventana en el canvas que la mostrará.
            ventana.blit(fondo_juego, (0, 0))
            # Mostrar la ventana dibujada (cambiar buffers, buffer pantalla a disponible para dibujar y viceversa).
            pygame.display.flip()


if __name__ == '__main__':
    klondike()

Tras la ejecución de este script obtenemos lo siguiente:
Ahora, voy a dibujar una carta (por ejemplo, el As de corazones) y los sitios para ir apilando las cartas de los difentes palos de la baraja francesa o de póker (corazones, diamantes, picas y tréboles). Para ello creo las respectivas clases ('Palo' y 'Carta').

Uso grupos de sprites, tanto para la carta (grupo indiviual) como para los sitios de los palos, por las facilidades que esto proporciona de cara a dibujarlos y detectar las colisiones.

El código hasta el momento quedaría como sigue:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# TUTORIAL: KLONDIKE(I).
#
# Pygame es una colección de módulos Python diseñados para crear videojuegos.
#
# Autor: Mikel García Larragan.
# https://mikelgarcialarragan.blogspot.com/

import pygame
import os


class Palo(pygame.sprite.Sprite):
    # Los palos de las cartas del mazo estará definidos por la clase Palo.
    def __init__(self, palo, x, y):
        super().__init__()
        self.palo = palo + 1
        self.x, self.y = x, y
        self.image = pygame.image.load('F_' + str(self.palo) + '.png').convert_alpha()
        self.rect = self.image.get_rect(center=(self.x, self.y))


class Carta(pygame.sprite.Sprite):
    # Las cartas estarán definidas por la clase Carta.
    def __init__(self):
        super().__init__()
        self.image = pygame.image.load('F_1_1.png').convert_alpha()

    def mover(self, x, y):
        # Método que permite mover las cartas.
        self.x, self.y = x, y
        self.rect = self.image.get_rect(center=(self.x, self.y))


def klondike():
    while True:
        # inicializar módulos internos de pygame.
        pygame.init()
        # Obtener las dimensiones de la pantalla.
        informacion_pantalla = pygame.display.Info()
        # Establecer tamaño de la ventana.
        ancho_pantalla = informacion_pantalla.current_w - int(informacion_pantalla.current_w * 0.5)
        alto_pantalla = informacion_pantalla.current_h - int(informacion_pantalla.current_h * 0.45)
        # crear una superficie de visualización: la ventana, y centrarla.
        os.environ['SDL_VIDEO_CENTERED']='1'
        ventana = pygame.display.set_mode((ancho_pantalla,alto_pantalla))
        # Título de la ventana.
        pygame.display.set_caption('Klondike')
        # Icono de la ventana.
        # Cargar la imagen del icono.
        icono = pygame.image.load('klondike.ico')
        # Establecer el icono de la ventana en tiempo de ejecución.
        pygame.display.set_icon(icono)
        # Cargar la imagen del fondo del juego.
        fondo_juego = pygame.image.load('fondo_juego.png')
        # Cambiar el formato de píxel del fondo del juego para crear una copia que se dibujará más rápidamente en la pantalla.
        fondo_juego = fondo_juego.convert()
        # Cargar archivos de fuentes y sonidos.
        sonido_click = pygame.mixer.Sound('click.wav')
        # Crear canal para sonidos.
        canal1 = pygame.mixer.Channel(0)
        # Dibujar los sitios de las pilas de los palos e incluirlos en un grupo.
        grupo_palos = pygame.sprite.Group()
        x, y = ancho_pantalla - 485, alto_pantalla - 494
        for num_palo in range(4):
            palo = Palo(num_palo, x, y)
            grupo_palos.add(palo)
            x += 120
        # Grupo individual para la carta a arrastrar y soltar.
        grupo_carta = pygame.sprite.GroupSingle()
        carta = Carta()
        grupo_carta.add(carta)
        carta.mover(255, 100)
        # Reloj para ejecutar el juego.
        reloj = pygame.time.Clock()
        # El juego se ejecutará a 60 frames por segundo.
        fps = 60
        # Bucle juego.
        while True:
            # Ejecutar el siguiente frame.
            reloj.tick(fps)
            # Escuchar cada uno de eventos de la lista de eventos de Pygame.
            for evento in pygame.event.get():
                # Finalizar el juego cuando se produzca el evento de terminación del programa (cierre de la ventana).
                if evento.type == pygame.QUIT:
                    pygame.quit()
                    return
            # Actualiza sprites.
            grupo_palos.update()
            grupo_carta.update()
            # Copiar la imagen de fondo de la ventana en el canvas que la mostrará.
            ventana.blit(fondo_juego, (0, 0))
            # dibujar sprites.
            grupo_palos.draw(ventana)
            grupo_carta.draw(ventana)
            # Mostrar la ventana dibujada (cambiar buffers, buffer pantalla a disponible para dibujar y viceversa).
            pygame.display.flip()


if __name__ == '__main__':
    klondike()

Y tras su ejecución obtenemos:
Ahora, ya estoy en disposición de implementar la funcionalidad de arrastrar y soltar ('drag and drop'). Básicamente, lo que quiero hacer es que cuando se realize clic del botón izquierdo del ratón sobre la carta, y hasta que se deje de pulsar dicho botón, la carta se arrastre (siga al puntero del ratón). Cuando la carta se suelte, es decir, deje de pulsarse el botón izquierdo del ratón, si la carta colisiona (se superpone parcial o totalmente) con alguno de los sitios de los palos, se quede allí, mientras que si no se produce dicha colisión, vuelva a su posición inicial.

Para ello, debo conocer cómo:

1.- Detectar que se ha pulsado o liberado el botón izquierdo del ratón, y realizar el seguimiento del cursor del ratón:

Para esto se pueden utilizar las funciones del ratón  (ver documentación).

Cuando se establece el modo de visualización, la cola de eventos comienza a a recibir eventos del ratón, y cuando se presionan y liberan los botones de éste se generan eventos MOUSEBUTTONDOWN y MOUSEBUTTONUP, respectivamente.

Estos eventos contienen un atributo que representa qué botón se presionó o liberó: 1 para el botón izquierdo, 2 para el central, 3 para el derecho, y 4 y 5 para la rueda.

Por otra parte, cada vez que se mueve el ratón se genera un evento MOUSEMOTION, que devuelve la posición actual del ratón cada vez que se mueve y, por tanto, sirve para rastrear o seguir el movimiento del mismo.

Es decir:

# Comprobar si se ha presionado o liberado el botón izquierdo del ratón.
if evento.type == pygame.MOUSEBUTTONDOWN:
    if evento.button == 1:
        print('Se ha pulsado el botón izquierdo del ratón')
elif evento.type == pygame.MOUSEBUTTONUP:
    if evento.button == 1:
        print('Se ha liberado el botón izquierdo del ratón')
# Seguimiento del movimiento del ratón.
elif evento.type == pygame.MOUSEMOTION:
    print('rastreando el movimiento del ratón', evento.pos)
2.- Obtener la posición del ratón y comprobar, si cuando se hace clic del botón izquierdo del ratón, éste se realiza sobre la carta:

Para obtener la posición del cursor del ratón cuando se hace clic sobre el botón izquierdo basta con usar el método .mouse.get_pos(), que devuelve las coordenadas x e y de la posición del puntero del ratón, mientras que para detectar la colisión entre la posición del cursor del ratón y la carta en el momento en que se hace click sobre el botón izquierdo, utilizo el método .collidepoint(), que devuelve verdadero si el punto indicado está dentro del rectángulo, es decir, en nuestro caso si el puntero del ratón se encuentra dentro de la carta en el momento de hacer clic con el botón izquierdo.
if evento.type == pygame.MOUSEBUTTONDOWN:
    if evento.button == 1:
        # Obtener posición del puntero del ratón.
        posicion_raton = pygame.mouse.get_pos()
        # Comprobar si se ha realizado click del botón izquierdo del ratón sobre la carta.
        if carta.rect.collidepoint(posicion_raton):
            canal1.play(sonido_click)
            carta_a_arrastrar_x, carta_a_arrastrar_y = carta.rect.center[0], carta.rect.center[1]
            arrastrando_carta = True
Como se puede observar en el código anterior, además de lo indicado he añadido dos variables para guardar la posición de la carta en el momento en el que se comienza a arrastrar ésta, y una variable boolena para indicar que se está arrastrando.

3.- Detectar si, cuando se libera o suelta el botón izquierdo del ratón, la carta colisiona con alguno/s de los sitios de los palos:

Para ello, utilizo el método .colliderect(), que devuelve verdadero si dos rectángulos se superponen total o parcialmente; en nuestro caso si la carta se superpone total o parcialmente con alguno/s de los sitios de los palos en el momento de liberar o soltar el botón izquierdo.
if evento.type == pygame.MOUSEBUTTONUP:
    if evento.button == 1 and arrastrando_carta:
        for sitio_palo in grupo_palos:
            if carta.rect.colliderect(sitio_palo):
                colision_palo = True
        if not colision_palo:
            carta.mover(carta_a_arrastrar_x, carta_a_arrastrar_y)
        else:
            colision_palo = False
        arrastrando_carta = False
Como se observa en el código anterior, se ha añadido una variable boolena para indicar si se produce la colisión entre la carta y alguno/s de los sitios de los palos y, si ésta se produce, una llamada al método .mover() de nuestra clase 'Carta', para que la carta se mueva con el cursor del ratón.


Poniendo junto todo lo anterior obtenemos:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# TUTORIAL: KLONDIKE(I).
#
# Pygame es una colección de módulos Python diseñados para crear videojuegos.
#
# Autor: Mikel García Larragan.
# https://mikelgarcialarragan.blogspot.com/

import pygame
import os


class Palo(pygame.sprite.Sprite):
    # Los palos de las cartas del mazo estará definidos por la clase Palo.
    def __init__(self, palo, x, y):
        super().__init__()
        self.palo = palo + 1
        self.x, self.y = x, y
        self.image = pygame.image.load('F_' + str(self.palo) + '.png').convert_alpha()
        self.rect = self.image.get_rect(center=(self.x, self.y))


class Carta(pygame.sprite.Sprite):
    # Las cartas estarán definidas por la clase Carta.
    def __init__(self):
        super().__init__()
        self.image = pygame.image.load('F_1_1.png').convert_alpha()

    def mover(self, x, y):
        # Método que permite mover las cartas.
        self.x, self.y = x, y
        self.rect = self.image.get_rect(center=(self.x, self.y))


def klondike():
    while True:
        # inicializar módulos internos de pygame.
        pygame.init()
        # Obtener las dimensiones de la pantalla.
        informacion_pantalla = pygame.display.Info()
        # Establecer tamaño de la ventana.
        ancho_pantalla = informacion_pantalla.current_w - int(informacion_pantalla.current_w * 0.5)
        alto_pantalla = informacion_pantalla.current_h - int(informacion_pantalla.current_h * 0.45)
        # crear una superficie de visualización: la ventana, y centrarla.
        os.environ['SDL_VIDEO_CENTERED']='1'
        ventana = pygame.display.set_mode((ancho_pantalla,alto_pantalla))
        # Título de la ventana.
        pygame.display.set_caption('Klondike')
        # Icono de la ventana.
        # Cargar la imagen del icono.
        icono = pygame.image.load('klondike.ico')
        # Establecer el icono de la ventana en tiempo de ejecución.
        pygame.display.set_icon(icono)
        # Cargar la imagen del fondo del juego.
        fondo_juego = pygame.image.load('fondo_juego.png')
        # Cambiar el formato de píxel del fondo del juego para crear una copia que se dibujará más rápidamente en la pantalla.
        fondo_juego = fondo_juego.convert()
        # Cargar archivos de fuentes y sonidos.
        sonido_click = pygame.mixer.Sound('click.wav')
        # Crear canal para sonidos.
        canal1 = pygame.mixer.Channel(0)
        # Dibujar los sitios de las pilas de los palos e incluirlos en un grupo.
        grupo_palos = pygame.sprite.Group()
        x, y = ancho_pantalla - 485, alto_pantalla - 494
        for num_palo in range(4):
            palo = Palo(num_palo, x, y)
            grupo_palos.add(palo)
            x += 120
        # Grupo individual para la carta a arrastrar y soltar.
        grupo_carta = pygame.sprite.GroupSingle()
        carta = Carta()
        grupo_carta.add(carta)
        carta.mover(255, 100)
        # Variables booleanas que indican si se está arrastrando la carta y si hay colision con uno o más de los sitios de
        # los palos, respectivamente.
        arrastrando_carta, colision_palo = False, False
        # Reloj para ejecutar el juego.
        reloj = pygame.time.Clock()
        # El juego se ejecutará a 60 frames por segundo.
        fps = 60
        # Bucle juego.
        while True:
            # Ejecutar el siguiente frame.
            reloj.tick(fps)
            # Escuchar cada uno de eventos de la lista de eventos de Pygame.
            for evento in pygame.event.get():
                # Finalizar el juego cuando se produzca el evento de terminación del programa (cierre de la ventana).
                if evento.type == pygame.QUIT:
                    pygame.quit()
                    return
                # Comprobar si se ha presionado o liberado el botón izquierdo del ratón.
                elif evento.type == pygame.MOUSEBUTTONDOWN:
                    if evento.button == 1:
                        # Obtener posición del puntero del ratón.
                        posicion_raton = pygame.mouse.get_pos()
                        # Comprobar si se ha realizado click del botón izquierdo del ratón sobre la carta.
                        if carta.rect.collidepoint(posicion_raton):
                            canal1.play(sonido_click)
                            carta_a_arrastrar_x, carta_a_arrastrar_y = carta.rect.center[0], carta.rect.center[1]
                            arrastrando_carta = True
                elif evento.type == pygame.MOUSEBUTTONUP:
                    if evento.button == 1 and arrastrando_carta:
                        for sitio_palo in grupo_palos:
                            if carta.rect.colliderect(sitio_palo):
                                colision_palo = True
                        if not colision_palo:
                            carta.mover(carta_a_arrastrar_x, carta_a_arrastrar_y)
                        else:
                            colision_palo = False
                        arrastrando_carta = False
                # Seguimiento del movimiento del ratón.
                elif evento.type == pygame.MOUSEMOTION:
                    if arrastrando_carta:
                        posicion_raton_x, posicion_raton_y = evento.pos
                        carta.mover(posicion_raton_x, posicion_raton_y)
            # Actualiza sprites.
            grupo_palos.update()
            grupo_carta.update()
            # Copiar la imagen de fondo de la ventana en el canvas que la mostrará.
            ventana.blit(fondo_juego, (0, 0))
            # dibujar sprites.
            grupo_palos.draw(ventana)
            grupo_carta.draw(ventana)
            # Mostrar la ventana dibujada (cambiar buffers, buffer pantalla a disponible para dibujar y viceversa).
            pygame.display.flip()


if __name__ == '__main__':
    klondike()
Si lo ejecutamos:
Y hasta aquí esta primera entrega del tutorial sobre el solitario Klondike. En posteriores entradas abordaré aspectos tales como: apilar las cartas en la pila del palo que les correponde, barajar el mazo, situar las cartas en las siete columnas, ir descubriendo las cartas del mazo...

Quizás también te interese:

Comentarios

Entradas populares de este blog

Criptografía (I): cifrado Vigenère y criptoanálisis Kasiski

Hace unos días mi amigo Iñaki Regidor ( @Inaki_Regidor ), a quien dedico esta entrada :), compartió en las redes sociales un post titulado "Criptografía: el arte de esconder mensajes"  publicado en uno de los blogs de EiTB . En ese post se explican ciertos métodos clásicos para cifrar mensajes , entre ellos el cifrado de Vigenère , y , al final del mismo, se propone un reto consistente en descifrar un mensaje , lo que me ha animado a escribir este post sobre el método Kasiski  para atacar un cifrado polialfabético ( conociendo la clave descifrar el mensaje es muy fácil, pero lo que contaré en este post es la forma de hacerlo sin saberla ). El mensaje a descifrar es el siguiente: LNUDVMUYRMUDVLLPXAFZUEFAIOVWVMUOVMUEVMUEZCUDVSYWCIVCFGUCUNYCGALLGRCYTIJTRNNPJQOPJEMZITYLIAYYKRYEFDUDCAMAVRMZEAMBLEXPJCCQIEHPJTYXVNMLAEZTIMUOFRUFC Como ya he dicho el método de Vigenère es un sistema de sustitución polialfabético , lo que significa que, al contrario que en un sistema...

Criptografía (XXIII): cifrado de Hill (I)

En este post me propongo explicar de forma comprensible lo que he entendido sobre el cifrado de Hill , propuesto por el matemático Lester S. Hill , en 1929, y que se basa en emplear una matriz como clave  para cifrar un texto en claro y su inversa para descifrar el criptograma correspondiente . Hay tres cosas que me gustan de la criptografía clásica, además de que considero que ésta es muy didáctica a la hora de comprender los sistemas criptográficos modernos: la primera de ellas es que me "obliga" a repasar conceptos de matemáticas aprendidos hace mucho tiempo y, desgraciadamente, olvidados también hace demasiado tiempo, y, por consiguiente, que, como dice  Dani , amigo y coautor de este blog, me "obliga" a hacer "gimnasia mental"; la segunda es que, en la mayoría de las ocasiones, pueden cifrarse y descifrase los mensajes, e incluso realizarse el criptoanálisis de los criptogramas, sin más que un simple lápiz y papel, es decir, para mi es como un pasat...

¿Qué significa el emblema de la profesión informática? (I)

Todas o muchas profesiones tienen un emblema que las representa simbólicamente y en el caso de la  informática: " es el establecido en la resolución de 11 de noviembre de 1977  para las titulaciones universitarias superiores de informática, y  está constituido por una figura representando en su parte central  un  núcleo toroidal de ferrita , atravesado por  hilos de lectura,  escritura e inhibición . El núcleo está rodeado por  dos ramas : una  de  laurel , como símbolo de recompensa, y la otra, de  olivo , como  símbolo de sabiduría. La  corona  será la  de la casa real  española,  y bajo el escudo se inscribirá el acrónimo de la organización. ". Veamos los diferentes elementos tomando como ejemplo el emblema del COIIE/EIIEO (Colegio Oficial de Ingenieros en Informática del País Vasco/ Euskadiko Informatikako Ingeniarien Elkargo Ofiziala ) . Pero no sólo el COIIE/EIIEO adopta el emblem...