Ir al contenido principal

Python + Pygame (VIII): Tutorial - Klondike (III)

En la primerasegunda entregas del tutorial correspondientes al juego de cartas Klondike (la variante más popular del solitario) expliqué la forma en la que he implementado las funcionalidades de arrastrar y soltar cartas ('drag and drop') y de barajar el mazo e ir descubriendo cartas del mismo, respectivamente, mientras que en este tercer post del tutorial voy a explicar detalladamente las funcionalidades de distribuir las cartas del mazo en las diferentes columnas y de apilar las cartas en sus correspondientes palos, con independencia de si se apilan desde las cartas descubiertas del mazo o desde las columnas. 

Como vengo comentando, publicaré mi primera versión de este juego una vez publique los cuatro tutoriales relativos a él, 

En esta entrega del tutorial voy a utilizar la baraja vasca. En la primera versión del juego se podrá jugar con ésta, la baraja francesa o de póker y la baraja española.

Parto del script obtenido en la segunda parte de este tutorial:

Para utilizar la baraja vasca, modifico nuestra clase 'Mazo' para cargar las imágenes de las cartas:
for palo in range(1, 5):
    for carta in range(1, 11):
        self.mazo.append('V_' + str(carta) + '_' + str(palo) + '.png')
Y la clase 'Palo', para que se dibujen los palos de la baraja vasca:
self.image = pygame.image.load('V_' + str(self.palo) + '.png').convert_alpha()
Es decir, lo único que hay que hacer es cambiar 'E' por 'V' a la hora de cargar las imágenes.

Y, ahora, añado lo siguiente:

1.- La distribución en columnas de las cartas del mazo: tras barajarse el mazo (ver parte 2 de este tutorial), las 28 primeras cartas se distribuyen en 7 columnas (1 carta en la primera, 2 en la segunda, 3 en la tercera,... y 7 en la séptima).

Lo primero que hago es modificar el método .mover() de nuestra clase 'Carta', para que considere situar las cartas boca abajo o boca arriba (descubierta):
def mover(self, x, y, descubierta):
    # Método que permite mover las cartas.
    self.descubierta = descubierta
    if self.descubierta:
        self.image = pygame.image.load(self.mazo.mazo[self.num_carta]).convert_alpha()
    else:
        self.image = pygame.image.load('reverso.png').convert_alpha()
    self.x, self.y = x, y
    self.rect = self.image.get_rect(center=(self.x, self.y))
Y, después: 
# Lista y grupo para las cartas del mazo encolumnadas.
lista_cartas_columnas = []
grupo_cartas_columnas = pygame.sprite.Group()
# Distribuir por columnas las 28 primeras cartas del mazo.
x, y = ancho_pantalla - 845, alto_pantalla - 350
columna, cartas_en_columna = 1, 0
for num_carta in range(28):
    carta = Carta(mazo, num_carta)
    grupo_cartas_columnas.add(carta)
    cartas_en_columna += 1
    if cartas_en_columna == columna:
        carta.mover(x, y, True)
        lista_cartas_columnas.append((carta, mazo.mazo[num_carta], True, True))
        columna += 1
        cartas_en_columna = 0
        x, y = x + 120, alto_pantalla - 350
    else:
        carta.mover(x, y, False)
        lista_cartas_columnas.append((carta, mazo.mazo[num_carta], False, False))
        y += 30
print('Cartas encolumnadas:('+ str(len(lista_cartas_columnas)) + '):', lista_cartas_columnas)
print('------------------------------------------------------')
Como se observa, para ello sólo se necesita un único bucle, que controla la posición (coordenadas x e y) de las diferentes columnas, el número de cartas a situar en cada columna (igual que el número de la columna) y que la última carta de cada columna se muestre descubierta, y se encarga de la llamada al método .mover() de nuestra clase 'Carta', que es el encargado de situarlas.

Después de ejecutar el script así obtenido, el resultado de las sentencias print es el siguiente:

Cartas encolumnadas:(28): [(<Carta Sprite(in 1 groups)>, 'V_8_4.png', True, True), (<Carta Sprite(in 1 groups)>, 'V_10_4.png', False, False), (<Carta Sprite(in 1 groups)>, 'V_9_1.png', True, True), (<Carta Sprite(in 1 groups)>, 'V_1_2.png', False, False), (<Carta Sprite(in 1 groups)>, 'V_2_4.png', False, False), (<Carta Sprite(in 1 groups)>, 'V_4_1.png', True, True), (<Carta Sprite(in 1 groups)>, 'V_9_3.png', False, False), (<Carta Sprite(in 1 groups)>, 'V_8_2.png', False, False), (<Carta Sprite(in 1 groups)>, 'V_1_4.png', False, False), (<Carta Sprite(in 1 groups)>, 'V_5_4.png', True, True), (<Carta Sprite(in 1 groups)>, 'V_7_3.png', False, False), (<Carta Sprite(in 1 groups)>, 'V_3_4.png', False, False), (<Carta Sprite(in 1 groups)>, 'V_2_3.png', False, False), (<Carta Sprite(in 1 groups)>, 'V_8_1.png', False, False), (<Carta Sprite(in 1 groups)>, 'V_1_1.png', True, True), (<Carta Sprite(in 1 groups)>, 'V_10_3.png', False, False), (<Carta Sprite(in 1 groups)>, 'V_3_1.png', False, False), (<Carta Sprite(in 1 groups)>, 'V_5_3.png', False, False), (<Carta Sprite(in 1 groups)>, 'V_10_2.png', False, False), (<Carta Sprite(in 1 groups)>, 'V_7_4.png', False, False), (<Carta Sprite(in 1 groups)>, 'V_7_2.png', True, True), (<Carta Sprite(in 1 groups)>, 'V_3_3.png', False, False), (<Carta Sprite(in 1 groups)>, 'V_9_4.png', False, False), (<Carta Sprite(in 1 groups)>, 'V_6_2.png', False, False), (<Carta Sprite(in 1 groups)>, 'V_3_2.png', False, False), (<Carta Sprite(in 1 groups)>, 'V_4_2.png', False, False), (<Carta Sprite(in 1 groups)>, 'V_7_1.png', False, False), (<Carta Sprite(in 1 groups)>, 'V_8_3.png', True, True)]
------------------------------------------------------

Tal y como se observa, las 28 primeras cartas del mazo, tras barajarlo, se han dispuesto en 7 columnas.

En cada carta:

- V: indica baraja vasca.
- Primer número: indica el valor de la carta (desde el 1, el As, hasta el 13, el Rey).
- Segundo número: indica el palo de la carta (1: Oros; 2: Copas; 3: Espadas y 4: Bastos).
- Primera variable booleana ('True' o 'False'): indica si la carta está boca abajo o descubierta.
- Segunda variable booleana ('True o 'False): indica si la carta es la última de la columna.

Es decir, tras haberse barajado el mazo, la primera columna contiene: Sota de bastos (descubierta), la segunda columa contiene: Rey de bastos (boca abajo) y caballo de oros (descubierta) ,...
2.- Lo necesario para detectar un doble clic: para apilar cartas, el jugador debe realizar doble clic, bien sobre la última carta descubierta del mazo, o bien sobre la última carta de una columna.

Pues bien, entre los eventos del ratón no existe uno específico para el doble clic, por lo que para detectarlo parto del evento MOUSEBUTTONDOWN, que indica que se ha hecho clic.

Antes que nada, completo las variables booleanas del script, para indicar la realización de clic's y doble clkic's, quedando éstas de la siguiente manera:
# Variables booleanas que indican si se ha hecho click sobre el mazo, sobre la última carta descubierta
# del mazo, sobre una carta de una pila y sobre una carta de una columna, respectivamente.
mazo_clickado, carta_mazo_clickada, carta_pila_clickada, carta_columna_clickada = False, False, False, False 
# Variables booleanas que indican si se ha hecho doble click sobre la última carta descubierta del mazo
# y sobre una carta de una columna, respectivamente.
carta_mazo_doble_click, carta_columna_doble_click = False, False
Después, en primer lugar, creo un objeto reloj para realizar el seguimiento o control del transcurso de una cantidad de tiempo (en milisegundos), y establezo el tiempo máximo (en milisegundos) que puede transcurrir entre dos clics para que esos dos clics consecutivos se consideren un doble clic (pongo 250 milisegundos). Es decir, si entre dos clics consecutivos transcurren menos de 250 milisegundos estos serán considerados como un doble clic, y, en caso contrario, serán considerados como dos clics individuales.
# Reloj para detectar el doble click del botón izquierdo del ratón.
doble_click = pygame.time.Clock()
# tiempo máximo (en milisegundos) entre dos clicks para que estos se consideren como doble click.
tiempo_doble_click = 250
Y, después, para detectar un doble clic del botón izquierdo del ratón, tanto sobre última carta descubierta del mazo como sobre la ultima carta de una columna, modifico la escucha de los eventos de la siguiente manera:
# 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 realizado click del 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 el
            # mazo de cartas.
            if mazo.rect.collidepoint(posicion_raton):
                canal1.play(sonido_click)
                mazo_clickado = True
            # Comprobar si se ha realizado doble click o sólo click del botón izquierdo del
            # ratón sobre la última carta descubierta del mazo.
            elif ultima_carta_descubierta != None and lista_cartas_descubiertas[-1][0]. \
                rect.collidepoint(posicion_raton):
                canal1.play(sonido_click)
                if doble_click.tick() < tiempo_doble_click:
                    carta_mazo_doble_click = True
                else:
                    carta_mazo_clickada = True   
            # Comprobar si se ha realizado doble click o sólo click del botón izquierdo del
            # ratón sobre alguna de las cartas descubiertas de las columnas.
            else:
                for carta_encolumnada in range(len(lista_cartas_columnas)):
                    if lista_cartas_columnas[carta_encolumnada][0].rect.collidepoint(
                        posicion_raton) and lista_cartas_columnas[carta_encolumnada][2]:
                        canal1.play(sonido_click)
                        if doble_click.tick() < tiempo_doble_click:
                            carta_columna_doble_click = True
                        else:
                            carta_columna_clickada = True
                        break
                # Comprobar si la carta sobre la que se ha realizado el doble click
                # es la última de la columna.
                if carta_columna_doble_click and lista_cartas_columnas[carta_encolumnada][3]:
                    pass
                else:
                    carta_columna_doble_click = False
Tal y como se observa, para comprobar si se ha realizado clic o doble clic sobre una carta, se utiliza 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 ultima carta descubierta del mazo o dentro de alguna de las cartas descubiertas de las columnas.

Además, comentar que para detectarse el doble clic sobre la carta descubierta de una columna, hay un bucle que comprueba la colisión entre el puntero del ratón y todas las cartas descubiertas de la columna y, si encuentra alguna, lo indica y sale del bucle.

En este último caso, también hay que tener en cuenta que para que la carta se pueda apilar debe ser la última carta de la columna, ya que en una columna puede haber varias cartas descubiertas y sólo se puede apilar la última.

3.- Lo necesario para apilar las cartas cuando se produzca un doble click sobre una carta:

Cuando se realice un doble clic, bien sobre la última carta descubierta del mazo, o bien sobre la última carta de una columna, se intentará apilar la carta en la pila del palo que le corresponda. Recordar que las cartas sólo se pueden apilar en orden ascendente, del As al Rey.

Lo primero que hago es crear una lista con las últimas cartas apiladas de cada palo, que servirá para ver si se cumple el último aspecto mencionado, es decir, para comprobar si las cartas de cada palo se van apilando en orden estríctamente ascendente (para apilar una carta la última carta apilada del palo correpondiente debe ser la de valor inmediatamente inferior):
# lista con las últimas cartas apiladas de cada palo.
lista_ultimas_cartas_apiladas_palo = []
for num_palo in range(4):
    lista_ultimas_cartas_apiladas_palo.append('0_' + str(num_palo + 1) + '.png')
Y ya sólo queda actuar conforme a los eventos producidos y que se han indicado anteriormente.

Es decir, en el caso del doble clic sobre la última carta descubierta del mazo:
elif carta_mazo_doble_click:
    # Intentar apilar la carta descubierta del mazo.
    if len(ultima_carta_descubierta[1]) == 9:
        valor_carta_a_apilar = ultima_carta_descubierta[1][2:3]
    elif len(ultima_carta_descubierta[1]) == 10:
        valor_carta_a_apilar = ultima_carta_descubierta[1][2:4]
    if str(int(valor_carta_a_apilar) - 1) + '_' + str(ultima_carta_descubierta[1][-5:-4]) + '.png' \
           == lista_ultimas_cartas_apiladas_palo[int(ultima_carta_descubierta[1][-5:-4]) -1]:
        ultima_carta_descubierta[0].mover(ancho_pantalla - 485 + (int(
                                          ultima_carta_descubierta[1][-5:-4]) - 1) * 120,
                                          alto_pantalla - 494, True)
        lista_ultimas_cartas_apiladas_palo[int(ultima_carta_descubierta[1][-5:-4]) -1] = \
            str(int(valor_carta_a_apilar)) + '_' + str(ultima_carta_descubierta[1][-5:-4]) + '.png'
        # La carta se ha apilado:
        # Agregar la carta apilada a la lista y al grupo de cartas apiladas.
        lista_cartas_apiladas.append((ultima_carta_descubierta))
        grupo_cartas_apiladas.add(ultima_carta_descubierta[0])
        # Eliminar la carta apilada del mazo.
        mazo.mazo.remove(ultima_carta_descubierta[1])
        # Eliminar la carta apilada de la lista y del grupo de cartas descubiertas.
        lista_cartas_descubiertas.remove(ultima_carta_descubierta)
        grupo_cartas_descubiertas.remove(ultima_carta_descubierta[0])
        # Poner como última carta descubierta a la de la lista de cartas descubiertas después de
        # eliminar de ella a la carta apilada.
        ultima_carta_descubierta = lista_cartas_descubiertas[-1]
        if len(lista_cartas_descubiertas) > 2:
            # Tras eliminar la carta apilada de la lista de cartas descubiertas, reordenar la
            # posición en pantalla de las tres últimas cartas descubiertas.
            x = ancho_pantalla - 745
            for carta_descubierta in range(-3, 0, 1):
                lista_cartas_descubiertas[carta_descubierta][0].mover(x, alto_pantalla - 494, True)
                x += 20
        carta_a_descubrir -= 1
        cartas_a_descubrir -= 1
    carta_mazo_doble_click = False
Como se puede observar, en primer lugar obtengo el valor de la carta a apilar, para, a continuación, comprobar que la última carta apilada del palo correspondiente a la carta a apilar es la inmediatamente inferior al valor de ésta. Si es así, la carta se apila llamando al método .mover() de nuestra clase 'Carta' y se actualiza el elemento correspondiente de la lista de últimas cartas apiladas por palo con la carta recien apilada.

Posteriormente, si se ha apilado la carta:

- Se  agrega la carta apilada a la lista y al grupo de cartas apiladas.
- Se elimina la carta apilada del mazo.
- Se elimina la carta apilada de la lista y del grupo de cartas descubiertas.
- Se pone como última carta descubierta a la de la lista de cartas descubiertas después de eliminar de ella la carta apilada.
- Si hay más de dos cartas descubiertas, se reordenan, mediante el método .mover() de nuestra clase 'Carta', las cartas que se muestran en pantalla: la primera será la última descubierta anteriormente a la mano en la que se produce el apilamiento de la carta, la segunda será la primera de dicha mano y la tercera carta a mostrar será la segunda.

Mientras que en el caso de doble clic sobre la última carta de una columna y de forma análoga a lo anterior:
elif carta_columna_doble_click:
    # Intentar apilar la última carta de la columna.
    if len(lista_cartas_columnas[carta_encolumnada][1]) == 9:
        valor_carta_a_apilar = lista_cartas_columnas[carta_encolumnada][1][2:3]
    elif len(ultima_carta_descubierta[1]) == 10:
        valor_carta_a_apilar = lista_cartas_columnas[carta_encolumnada][1][2:4]
    if str(int(valor_carta_a_apilar) - 1) + '_' + str(lista_cartas_columnas[carta_encolumnada][1][-5:-4]) + '.png' \
           == lista_ultimas_cartas_apiladas_palo[int(lista_cartas_columnas[carta_encolumnada][1][-5:-4]) -1]:
        lista_cartas_columnas[carta_encolumnada][0].mover(ancho_pantalla - 485 + (int(
                                                          lista_cartas_columnas[carta_encolumnada][1][-5:-4]) - 1) * 120,
                                                          alto_pantalla - 494, True)
        lista_ultimas_cartas_apiladas_palo[int(lista_cartas_columnas[carta_encolumnada][1][-5:-4]) -1] = \
            str(int(valor_carta_a_apilar)) + '_' + str(lista_cartas_columnas[carta_encolumnada][1][-5:-4]) + '.png'
        # La carta se ha apilado:
        # Agregar la carta apilada a la lista y el grupo de cartas apiladas.
        lista_cartas_apiladas.append((lista_cartas_columnas[carta_encolumnada][0], lista_cartas_columnas[carta_encolumnada][1]))
        grupo_cartas_apiladas.add(lista_cartas_columnas[carta_encolumnada][0])
        # Descubrir, si hay y no está ya descubierta, la carta anterior de la columna:
        if carta_encolumnada != 0:
            if not lista_cartas_columnas[carta_encolumnada - 1][2]:
                lista_cartas_columnas[carta_encolumnada - 1][0].mover(lista_cartas_columnas[carta_encolumnada - 1][0].rect.center[0],
                                                                      lista_cartas_columnas[carta_encolumnada - 1][0].rect.center[1],
                                                                      True)
                carta_encolumnada_anterior = list(lista_cartas_columnas[carta_encolumnada - 1])
                carta_encolumnada_anterior[2], carta_encolumnada_anterior[3] = True, True
                lista_cartas_columnas[carta_encolumnada - 1] = tuple(carta_encolumnada_anterior)
        # Eliminar la carta apilada del grupo y de la lista de cartas encolumnadas.
        grupo_cartas_columnas.remove(lista_cartas_columnas[carta_encolumnada][0])
        lista_cartas_columnas.remove(lista_cartas_columnas[carta_encolumnada])
    carta_columna_doble_click = False
Es decir, al igual que en el caso del doble clic sobre la última carta descubierta del mazo, en primer lugar obtengo el valor de la carta a apilar, para, a continuación, comprobar que la última carta apilada del palo correspondiente a la carta a apilar es la inmediatamente inferior al valor de ésta. Si es así, la carta se apila llamando al método .mover() de nuestra clase 'Carta' y se actualiza el elemento correspondiente de la lista de últimas cartas apiladas por palo con la carta recien apilada.

Posteriormente, si se ha apilado la carta:

- Se  agrega la carta apilada a la lista y al grupo de cartas apiladas.
- Se descubre, si la hay y no está ya descubierta, la carta anterior de la columna, mediante el método .mover() de nuestra clase 'Carta', y se indica que la carta está descubierta y que es la última de la columna (valores 'True' en el segundo y tercer argumentos de la tupla de la carta anterior en la lista de cartas encolumnadas).
Sobre esto último,  cabe indicar, que las tuplas en python no son mutables, es decir, sus elementos no pueden modificarse directamente, por lo que como se observa en el código anterior, primero la tupla se convierte a lista, se modifican los atributos booleanos de esta lista, y, finalmente, la lista modificada se convierte en tupla y se actualiza con ella la lista de cartas encolumnadas. 
- Se elimina la carta apilada del grupo y de la lista de cartas encolumnadas.

Poniendo junto todo lo hecho hasta este momento obtenemos (no olvidarse de actualizar y dibujar los sprites al final del script):
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# TUTORIAL: KLONDIKE(III).
#
# Microsoft Windows (1990).
#
# 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
from random import shuffle


class Mazo(pygame.sprite.Sprite):
    # El mazo de cartas estará definido por la clase Mazo.
    def __init__(self, x, y):
        super().__init__()
        self.x, self.y = x, y
        self.mazo = []
        for palo in range(1, 5):
            for carta in range(1, 11):
                self.mazo.append('V_' + str(carta) + '_' + str(palo) + '.png')
        self.image = pygame.image.load('reverso.png').convert_alpha()
        self.rect = self.image.get_rect(center=(self.x, self.y))
    
    def barajar(self):
        # Método que permite barajar el mazo de cartas.
        shuffle(self.mazo)

    def vaciar(self):
        # Método que muestra la imagen del sitio del mazo vacío de cartas.
        self.image = pygame.image.load('sitio_mazo.png').convert_alpha()

    def reiniciar(self):
        # Método que muestra la imagen del sitio del mazo con las cartas a descubrir.
        self.image = pygame.image.load('reverso.png').convert_alpha()


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('V_' + 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, mazo, num_carta):
        super().__init__()
        self.mazo = mazo
        self.num_carta = num_carta
        self.image = pygame.image.load(self.mazo.mazo[self.num_carta]).convert_alpha()

    def mover(self, x, y, descubierta):
        # Método que permite mover las cartas.
        self.descubierta = descubierta
        if self.descubierta:
            self.image = pygame.image.load(self.mazo.mazo[self.num_carta]).convert_alpha()
        else:
            self.image = pygame.image.load('reverso.png').convert_alpha()
        self.x, self.y = x, y
        self.rect = self.image.get_rect(center=(self.x, self.y))

    def descubrir(self, orden):
        # Método que permite ir descubriendo las cartas del mazo.
        self.orden = orden
        if self.orden == 0:
            self.rect = self.image.get_rect(center=(self.mazo.x + 100, self.mazo.y))
        elif self.orden == 1:
            self.rect = self.image.get_rect(center=(self.mazo.x + 120, self.mazo.y))
        elif self.orden == 2:
            self.rect = self.image.get_rect(center=(self.mazo.x + 140, self.mazo.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 el mazo de cartas e incluirlo en un grupo individual.
        grupo_mazo = pygame.sprite.GroupSingle()
        mazo = Mazo(ancho_pantalla - 845, alto_pantalla - 494)
        grupo_mazo.add(mazo)
        # 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
        # Barajar el mazo.
        mazo.barajar()
        print('*** Mazo barajado:('+str(len(mazo.mazo))+'):', mazo.mazo)
        print('------------------------------------------------------')
        # Lista y grupo para las cartas del mazo encolumnadas.
        lista_cartas_columnas = []
        grupo_cartas_columnas = pygame.sprite.Group()
        # Distribuir por columnas las 28 primeras cartas del mazo.
        x, y = ancho_pantalla - 845, alto_pantalla - 350
        columna, cartas_en_columna = 1, 0
        for num_carta in range(28):
            carta = Carta(mazo, num_carta)
            grupo_cartas_columnas.add(carta)
            cartas_en_columna += 1
            if cartas_en_columna == columna:
                carta.mover(x, y, True)
                lista_cartas_columnas.append((carta, mazo.mazo[num_carta], True, True))
                columna += 1
                cartas_en_columna = 0
                x, y = x + 120, alto_pantalla - 350
            else:
                carta.mover(x, y, False)
                lista_cartas_columnas.append((carta, mazo.mazo[num_carta], False, False))
                y += 30
        print('Cartas encolumnadas:('+ str(len(lista_cartas_columnas)) + '):', lista_cartas_columnas)
        print('------------------------------------------------------')
        # Variables booleanas que indican si se ha hecho click sobre el mazo, sobre la última carta descubierta
        # del mazo, sobre una carta de una pila y sobre una carta de una columna, respectivamente.
        mazo_clickado, carta_mazo_clickada, carta_pila_clickada, carta_columna_clickada = False, False, False, False 
        # Variables booleanas que indican si se ha hecho doble click sobre la última carta descubierta del mazo
        # y sobre una carta de una columna, respectivamente.
        carta_mazo_doble_click, carta_columna_doble_click = False, False
        # Lista y grupo para las cartas que se van descubriendo del mazo.
        lista_cartas_descubiertas = []
        grupo_cartas_descubiertas = pygame.sprite.Group()
        # Variables que indican la siguiente carta del mazo a descubrir y si éste está vacio, respectivamente.
        carta_a_descubrir, mazo_vacio = 28, False
        # Parámetros de la baraja a utilizar:
        num_cartas = 40
        cartas_a_descubrir = num_cartas - 28
        # Última carta descubierta del mazo.
        ultima_carta_descubierta = None
        # Variable booleana que indica si se está arrastrando una o más cartas.
        arrastrando_cartas = False
        # Lista y grupo para las cartas apiladas.
        lista_cartas_apiladas = []
        grupo_cartas_apiladas = pygame.sprite.Group()
        # lista con las últimas cartas apiladas de cada palo.
        lista_ultimas_cartas_apiladas_palo = []
        for num_palo in range(4):
            lista_ultimas_cartas_apiladas_palo.append('0_' + str(num_palo + 1) + '.png')
        # Reloj para detectar el doble click del botón izquierdo del ratón.
        doble_click = pygame.time.Clock()
        # tiempo máximo (en milisegundos) entre dos clicks para que estos se consideren como doble click.
        tiempo_doble_click = 250
        # 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 realizado click del 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 el
                        # mazo de cartas.
                        if mazo.rect.collidepoint(posicion_raton):
                            canal1.play(sonido_click)
                            mazo_clickado = True
                        # Comprobar si se ha realizado doble click o sólo click del botón izquierdo del
                        # ratón sobre la última carta descubierta del mazo.
                        elif ultima_carta_descubierta != None and lista_cartas_descubiertas[-1][0]. \
                            rect.collidepoint(posicion_raton):
                            canal1.play(sonido_click)
                            if doble_click.tick() < tiempo_doble_click:
                                carta_mazo_doble_click = True
                            else:
                                carta_mazo_clickada = True   
                        # Comprobar si se ha realizado doble click o sólo click del botón izquierdo del
                        # ratón sobre alguna de las cartas descubiertas de las columnas.
                        else:
                            for carta_encolumnada in range(len(lista_cartas_columnas)):
                                if lista_cartas_columnas[carta_encolumnada][0].rect.collidepoint(
                                    posicion_raton) and lista_cartas_columnas[carta_encolumnada][2]:
                                    canal1.play(sonido_click)
                                    if doble_click.tick() < tiempo_doble_click:
                                        carta_columna_doble_click = True
                                    else:
                                        carta_columna_clickada = True
                                    break
                            # Comprobar si la carta sobre la que se ha realizado el doble click
                            # es la última de la columna.
                            if carta_columna_doble_click and lista_cartas_columnas[carta_encolumnada][3]:
                                pass
                            else:
                                carta_columna_doble_click = False
                # Comprobar si se ha soltado (liberado) el botón izquierdo del ratón.
                elif evento.type == pygame.MOUSEBUTTONUP:
                    if evento.button == 1:
                        arrastrando_cartas = False
                # Seguimiento del movimiento del ratón.
                elif evento.type == pygame.MOUSEMOTION:
                    if arrastrando_cartas:
                        posicion_raton_x, posicion_raton_y = evento.pos
            if mazo_clickado:
                if mazo_vacio:
                    lista_cartas_descubiertas = []
                    grupo_cartas_descubiertas.empty()
                    ultima_carta_descubierta = None
                    mazo.reiniciar()
                    mazo_vacio = False
                else:
                    for orden_carta in range(3):
                        carta = Carta(mazo, carta_a_descubrir)
                        carta.descubrir(orden_carta)
                        lista_cartas_descubiertas.append((carta, mazo.mazo[carta_a_descubrir]))
                        grupo_cartas_descubiertas.add(carta)
                        if len(lista_cartas_descubiertas) == cartas_a_descubrir:
                            mazo.vaciar()
                            mazo_vacio = True
                            carta_a_descubrir = 28
                            break
                        else:
                            carta_a_descubrir += 1
                    if len(lista_cartas_descubiertas) > 2:
                        # Ordenar la posición en pantalla de las tres últimas cartas descubiertas. 
                        x = ancho_pantalla - 745
                        for carta_descubierta in range(-3, 0, 1):
                            lista_cartas_descubiertas[carta_descubierta][0].mover(x, alto_pantalla - 494, True)
                            x += 20
                    ultima_carta_descubierta = lista_cartas_descubiertas[-1]
                mazo_clickado = False
            elif carta_mazo_doble_click:
                # Intentar apilar la carta descubierta del mazo.
                if len(ultima_carta_descubierta[1]) == 9:
                    valor_carta_a_apilar = ultima_carta_descubierta[1][2:3]
                elif len(ultima_carta_descubierta[1]) == 10:
                    valor_carta_a_apilar = ultima_carta_descubierta[1][2:4]
                if str(int(valor_carta_a_apilar) - 1) + '_' + str(ultima_carta_descubierta[1][-5:-4]) + '.png' \
                       == lista_ultimas_cartas_apiladas_palo[int(ultima_carta_descubierta[1][-5:-4]) -1]:
                    ultima_carta_descubierta[0].mover(ancho_pantalla - 485 + (int(
                                                      ultima_carta_descubierta[1][-5:-4]) - 1) * 120,
                                                      alto_pantalla - 494, True)
                    lista_ultimas_cartas_apiladas_palo[int(ultima_carta_descubierta[1][-5:-4]) -1] = \
                        str(int(valor_carta_a_apilar)) + '_' + str(ultima_carta_descubierta[1][-5:-4]) + '.png'
                    # La carta se ha apilado:
                    # Agregar la carta apilada a la lista y el grupo de cartas apiladas.
                    lista_cartas_apiladas.append((ultima_carta_descubierta))
                    grupo_cartas_apiladas.add(ultima_carta_descubierta[0])
                    # Eliminar la carta apilada del mazo.
                    mazo.mazo.remove(ultima_carta_descubierta[1])
                    # Eliminar la carta apilada de la lista y el grupo de cartas descubiertas.
                    lista_cartas_descubiertas.remove(ultima_carta_descubierta)
                    grupo_cartas_descubiertas.remove(ultima_carta_descubierta[0])
                    # Poner como última carta descubierta a la de la lista de cartas descubiertas después de
                    # eliminar de ella a la carta apilada.
                    ultima_carta_descubierta = lista_cartas_descubiertas[-1]
                    if len(lista_cartas_descubiertas) > 2:
                        # Tras eliminar la carta apilada de la la lista de cartas descubiertas, reordenar la
                        # posición en pantalla de las tres últimas cartas descubiertas.
                        x = ancho_pantalla - 745
                        for carta_descubierta in range(-3, 0, 1):
                            lista_cartas_descubiertas[carta_descubierta][0].mover(x, alto_pantalla - 494, True)
                            x += 20
                    carta_a_descubrir -= 1
                    cartas_a_descubrir -= 1
                carta_mazo_doble_click = False
            elif carta_mazo_clickada:
                carta_mazo_clickada = False
            elif carta_columna_doble_click:
                # Intentar apilar la última carta de la columna.
                if len(lista_cartas_columnas[carta_encolumnada][1]) == 9:
                    valor_carta_a_apilar = lista_cartas_columnas[carta_encolumnada][1][2:3]
                elif len(ultima_carta_descubierta[1]) == 10:
                    valor_carta_a_apilar = lista_cartas_columnas[carta_encolumnada][1][2:4]
                if str(int(valor_carta_a_apilar) - 1) + '_' + str(lista_cartas_columnas[carta_encolumnada][1][-5:-4]) + '.png' \
                       == lista_ultimas_cartas_apiladas_palo[int(lista_cartas_columnas[carta_encolumnada][1][-5:-4]) -1]:
                    lista_cartas_columnas[carta_encolumnada][0].mover(ancho_pantalla - 485 + (int(
                                                                      lista_cartas_columnas[carta_encolumnada][1][-5:-4]) - 1) * 120,
                                                                      alto_pantalla - 494, True)
                    lista_ultimas_cartas_apiladas_palo[int(lista_cartas_columnas[carta_encolumnada][1][-5:-4]) -1] = \
                        str(int(valor_carta_a_apilar)) + '_' + str(lista_cartas_columnas[carta_encolumnada][1][-5:-4]) + '.png'
                    # La carta se ha apilado:
                    # Agregar la carta apilada a la lista y el grupo de cartas apiladas.
                    lista_cartas_apiladas.append((lista_cartas_columnas[carta_encolumnada][0], lista_cartas_columnas[carta_encolumnada][1]))
                    grupo_cartas_apiladas.add(lista_cartas_columnas[carta_encolumnada][0])
                    # Descubrir, si hay y no está ya descubierta, la carta anterior de la columna:
                    if carta_encolumnada != 0:
                        if not lista_cartas_columnas[carta_encolumnada - 1][2]:
                            lista_cartas_columnas[carta_encolumnada - 1][0].mover(lista_cartas_columnas[carta_encolumnada - 1][0].rect.center[0],
                                                                                  lista_cartas_columnas[carta_encolumnada - 1][0].rect.center[1],
                                                                                  True)
                            carta_encolumnada_anterior = list(lista_cartas_columnas[carta_encolumnada - 1])
                            carta_encolumnada_anterior[2], carta_encolumnada_anterior[3] = True, True
                            lista_cartas_columnas[carta_encolumnada - 1] = tuple(carta_encolumnada_anterior)
                    # Eliminar la carta apilada del grupo y de la lista de cartas encolumnadas.
                    grupo_cartas_columnas.remove(lista_cartas_columnas[carta_encolumnada][0])
                    lista_cartas_columnas.remove(lista_cartas_columnas[carta_encolumnada])
                carta_columna_doble_click = False
            elif carta_columna_clickada:
                carta_columna_clickada = False
            # Actualiza sprites.
            grupo_mazo.update()
            grupo_palos.update()
            grupo_cartas_descubiertas.update()
            grupo_cartas_columnas.update()
            grupo_cartas_apiladas.update()
            # Copiar la imagen de fondo de la ventana en el canvas que la mostrará.
            ventana.blit(fondo_juego, (0, 0))
            # dibujar sprites.
            grupo_mazo.draw(ventana)
            grupo_palos.draw(ventana)
            grupo_cartas_descubiertas.draw(ventana)
            grupo_cartas_columnas.draw(ventana)
            grupo_cartas_apiladas.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 tercera entrega del tutorial sobre el solitario Klondike. En la cuarta y última entrada abordaré cómo arrastrar y soltar las cartas, bien desde las cartas descubiertas del mazo hasta una pila o hasta una columna, bien desde las cartas apiladas en los palos hasta una columna, o bien desde una columna a otra o a una pila.

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...