Ir al contenido principal

Python + Pygame (XXXIII): Tutorial - Conector de letras

En este post pongo un breve tutorial sobre otro de los aspectos a implementar en videojuegos 2D, aunque en esta ocasión se trata de algo que no nos encontraremos con frecuencia: conectar letras mediante líneas para formar palabras, es decir, de un conector de letras tipo 'Words of Wonders' (WoW).

Incluyo aquí este tutorial porque es un tema que me resulta curioso y porque estoy desarrollando este juego en python con pygame.

Se trata de un juego muy sencillo, en el que hay que hacer clic sobre una de las letras que se presentan y conectarla con otras letras para formar diferentes palabras. Una vez que se forme una palabra, si es correcta, se añadirá al crucigrama que se encuentra a la izquierda de las letras disponibles, y si se completa el crucigrama se avanzará al siguiente nivel.

Para implementar esto parto de los siguientes requisitos:

1.- La letra que inicia todas las conexiones entre letras se selecciona mediante clic del botón izquerdo del ratón.

2.- A partir de la letra anterior (la primera de las conexiones) y mientras se mantenga pulsado el botón izquierdo del ratón, se van conectado con una línea las letras sobre las que pase el ratón y, si en un momento dado se vuelve a la anteúltima conectada, se deshará (borrará) la última conexión, convirténdose la anteúltima en el inicio de la conexión en curso.

3.- Si se completase una palabra se comprobaría si es correcta y, si es así, se incluiría en el crucigrama que figura a la izquierda de las letras (ésto no se realiza en este pequeño tutorial, ya que éste pretende centrarse exclusivamente en el establecimiento de las conexiones), mientras que si se suelta el botón izquierdo del ratón sin completar una palabra se borrarán todas las conexiones realizadas hasta el momento.

4.- Mediante un icono situado en el centro del círculo donde están las letras, se pueden barajar o mezclar aleatoriamente estas últimas. Muchas veces, el cambiar la posición de las letras, nos puede facilitar encontrar las palabras correctas.

En 'Words of Wonders' (WoW), a medida que se avanza en el juego, se van descubriendo nuevas culturas y lugares icónicos (las maravillas), y en mi versión se irán descubriendo las maravillas del universo.

Dicho lo cual, el script realizado para ésto, es el siguiente:

1.- Importar las librerías necesarias:

import pygame
import os
from random import shuffle

El método shuffle se importa de la librería random para mezclar o barajar las letras. Este método toma una lista y baraja aleatoriamente sus elementos. Es importante tener en cuenta que este método modifica la lista original con los elementos reordenados.

2.- Inicializar pygame, crear ventana, cargar archivos de fuentes y sonidos, crear instancias de los objetos y los grupos de sprites, e inicializar variables:

def conector_letras():
    # Inicializar módulos internos de pygame.
    pygame.init()
    # Establecer tamaño de la ventana.
    ancho_pantalla, alto_pantalla = 1248, 702
    # 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('Conector de letras')
    # Cargar la imagen del fondo del nivel 1 y cambiar el formato de píxel para crear una copia que se dibujará más rápidamente en la
    # pantalla.
    fondo_1 = pygame.image.load('recursos/imagenes/fondo_1.png')
    fondo_1 = fondo_1.convert()
    # Fuentes.
    arial_40_negrita = pygame.font.SysFont('arial', 40, bold=True)
    arial_65_negrita = pygame.font.SysFont('arial', 65, bold=True)
    # Cargar archivos de sonidos.
    sonido_musica_fondo = pygame.mixer.Sound('recursos/sonidos/musica_fondo.wav')
    sonido_seleccionar = pygame.mixer.Sound('recursos/sonidos/seleccionar.wav')
    sonido_deshacer = pygame.mixer.Sound('recursos/sonidos/deshacer.wav')
    sonido_barajar = pygame.mixer.Sound('recursos/sonidos/barajar.wav')
    # Nivel del juego.
    nivel = 1
    # Crear canales para sonidos simultáneos.
    canal1 = pygame.mixer.Channel(0)
    canal2 = pygame.mixer.Channel(1)
    # Grupo individual para el botón que permite barajar las letras.
    grupo_boton_mezclar = pygame.sprite.GroupSingle()
    boton_mezclar = Boton_Mezclar(ancho_pantalla // 2 + 440, alto_pantalla // 2 + 20)
    grupo_boton_mezclar.add(boton_mezclar)
    # Círculos de las letras de las palabras a adivinar (nivel del juego, número de círculo, sprite del círculo, letra, coordenadas 'x' e 'y'
    # absolutas del círculo, y coordenadas 'x' e 'y' de la letra relativas al centro del círculo.
    circulos = [(1, 1, None, 'A', ancho_pantalla // 2 + 435, alto_pantalla // 2 - 60, None, None),
                (1, 2, None, 'I', ancho_pantalla // 2 + 508, alto_pantalla // 2 + 65, None, None),
                (1, 3, None, 'D', ancho_pantalla // 2 + 372, alto_pantalla // 2 + 65, None, None)]
    # Grupo de sprites. Agrupamos los círculos donde se inscriben las letras que forman parte de la palabra usando la clase sprite.Group de
    # Pygame para hacer que dicha clase se encargue de actualizarlos y dibujarlos, sin necesidad de hacerlo manualmente.
    grupo_circulos = pygame.sprite.Group()
    for circulo_letra in range(len(circulos)):
        if circulos[circulo_letra][0] > nivel - 1 and circulos[circulo_letra][0] < nivel + 1:
            circulo = Circulo(circulos[circulo_letra][4], circulos[circulo_letra][5])
            lista_tupla_circulos = list(circulos[circulo_letra])
            lista_tupla_circulos[2] = circulo
            if lista_tupla_circulos[3] == 'I':
                lista_tupla_circulos[6] = circulo.rect.center[0] - 8
            else:
                lista_tupla_circulos[6] = circulo.rect.center[0] - 18
            lista_tupla_circulos[7] = circulo.rect.center[1] - 40
            circulos[circulo_letra] = tuple(lista_tupla_circulos)
            grupo_circulos.add(circulo)
    # Grupo de sprites. Agrupamos los círculos que han sido seleccionados usando la clase sprite.Group de Pygame para hacer que dicha clase
    # se encargue de actualizarlos y dibujarlos, sin necesidad de hacerlo manualmente.
    grupo_circulos_seleccionados = pygame.sprite.Group()
    # Lista para las líneas que van conectando círculos en una misma conexión.
    lista_lineas = []
    # Palabra que se va a intentar como palabra a adivinar. 
    palabra = ''
    # Variable booleana que indica si se ha producido una colision con un círculo que no ha sido previamente seleccionado.
    colision_no_seleccionado = False
    # Variable booleana que indica si hay que barajar las letras.
    barajar = False
    # Hacer que suene la música de fondo del juego.
    canal1.play(sonido_musica_fondo, -1)
    # Reloj para ejecutar el videojuego.
    reloj = pygame.time.Clock()
    # El juego se ejecutará a 60 frames por segundo.
    fps = 60

3.- Bucle principal del juego:

    while True:
        # Ejecutar el siguiente frame.
        reloj.tick(fps)
        # Escuchar cada uno de los 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:
                    if len(grupo_circulos_seleccionados) == 0:
                        # 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 alguno de los circulos.
                        for circulo in range(len(circulos)):
                            desp_x =posicion_raton[0] - circulos[circulo][2].rect.x
                            desp_y =posicion_raton[1] - circulos[circulo][2].rect.y
                            # Verificar si el puntero está dentro de la máscara del círculo.
                            if circulos[circulo][2].rect.collidepoint(posicion_raton):
                                if circulos[circulo][2] not in grupo_circulos_seleccionados:
                                    if circulos[circulo][2].mask.get_at((desp_x, desp_y)):           
                                        canal2.play(sonido_seleccionar)
                                        # Incluir el círculo seleccionado en el grupo de círculos seleccionados, mostrar la imagen 
                                        # correspondiente a la selección de círculos y poner la letra como primera de la palabra que se va
                                        # a intentar que coincida con una de las que hay que adivinar.
                                        grupo_circulos_seleccionados.add(circulos[circulo][2])
                                        circulos[circulo][2].seleccionar()
                                        palabra = palabra + circulos [circulo][3]
                                        # Indicar que el círculo sobre el que se ha hecho click es el inicio de la conexión en curso, poner
                                        # el puntero del ratón en su centro y a este último como inicio de la línea de conexión.
                                        circulo_inicio = circulos[circulo][2]
                                        pygame.mouse.set_pos(circulo_inicio.rect.center[0], circulo_inicio.rect.center[1])
                                        inicio_linea = (circulo_inicio.rect.center[0], circulo_inicio.rect.center[1])
                                break
                    if len(grupo_circulos_seleccionados) == 0:
                        # Comprobar si se debe barajar.
                        if boton_mezclar.rect.collidepoint(posicion_raton) and not barajar:
                            barajar = True
            # Comprobar si se ha soltado (liberado) el botón izquierdo del ratón.
            elif evento.type == pygame.MOUSEBUTTONUP:
                if evento.button == 1:
                    if len(grupo_circulos_seleccionados) > 0:
                        # Eliminar los círculos seleccionados del grupo de círculos seleccionados y todas las líneas de conexión.
                        grupo_circulos_seleccionados.empty()
                        lista_lineas =[]
                        # Comprobar si se ha completado una palabra.
                        if len(palabra) == len(grupo_circulos):
                            print('*** Comprobar la palabra formada.')
                        # Borrar el contenido de la palabra a intentar que coincida con una de las que hay que adivinar. 
                        palabra = ''
                    else:
                        while barajar:
                            # Hacer una copia de las lista de circulos. Esta lista es la que será barajada.
                            circulos_barajados = circulos.copy()
                            # Barajar la lista de circulos_barajados (copia de la lista círculos).
                            boton_mezclar.barajar(circulos_barajados)
                            for circulo_barajado in range(len(boton_mezclar.circulos)):
                                if boton_mezclar.circulos[circulo_barajado][3] != circulos[circulo_barajado][3]:
                                    canal2.play(sonido_barajar)
                                    barajar = False
                                    break
                            if not barajar:
                                # Modificar las letras en los círculos conforme a lo obtenido al barajar.
                                for circulo_barajado in range(len(boton_mezclar.circulos)):
                                    lista_tupla_circulos = list(circulos[circulo_barajado])
                                    lista_tupla_circulos[3] = boton_mezclar.circulos[circulo_barajado][3]
                                    if lista_tupla_circulos[3] == 'I':
                                       lista_tupla_circulos[6] = lista_tupla_circulos[2].rect.center[0] - 8
                                    else:
                                       lista_tupla_circulos[6] = lista_tupla_circulos[2].rect.center[0] - 18
                                    lista_tupla_circulos[7] = lista_tupla_circulos[2].rect.center[1] - 40
                                    circulos[circulo_barajado] = tuple(lista_tupla_circulos)
            # Seguimiento del movimiento del ratón.
            elif evento.type == pygame.MOUSEMOTION:
                if len(grupo_circulos_seleccionados) > 0:
                    # Obtener posición del puntero del ratón.
                    posicion_raton = pygame.mouse.get_pos()
                    # Comprobar si el puntero del ratón colisiona con algún círculo que no esté previamente seleccionado.
                    for circulo in range(len(circulos)):
                         desp_x = posicion_raton[0] - circulos[circulo][2].rect.x
                         desp_y = posicion_raton[1] - circulos[circulo][2].rect.y
                         # Verificar si el puntero está dentro de la máscara del círculo.
                         if circulos[circulo][2].rect.collidepoint(posicion_raton):
                             if circulos[circulo][2] not in grupo_circulos_seleccionados:
                                 if circulos[circulo][2].mask.get_at((desp_x, desp_y)):
                                     canal2.play(sonido_seleccionar)
                                     # colision con un círculo que no ha sido previamente seleccionado.
                                     colision_no_seleccionado = True
                                     # Incluir el círculo seleccionado en el grupo de círculos seleccionados, mostrar la imagen
                                     # correspondiente a la selección de círculos y poner la letra como siguiente de la palabra
                                     # que se va a intentar que coincida con una de las que hay que adivinar.
                                     grupo_circulos_seleccionados.add(circulos[circulo][2])
                                     circulos[circulo][2].seleccionar()
                                     palabra = palabra + circulos [circulo][3]
                                     # Incluir la línea de conexión en la lista de líneas de conexión.
                                     lista_lineas.append((circulo_inicio.rect.center[0], circulo_inicio.rect.center[1],
                                                          circulos[circulo][2].rect.center[0], circulos[circulo][2].rect.center[1]))
                                     # Indicar que el círculo seleccionado es el inicio de la conexión en curso, poner el puntero del
                                     # ratón en su centro y a este último como inicio de la línea de conexión.
                                     circulo_inicio = circulos[circulo][2]
                                     pygame.mouse.set_pos(circulo_inicio.rect.center[0], circulo_inicio.rect.center[1])
                                     inicio_linea = (circulo_inicio.rect.center[0], circulo_inicio.rect.center[1])
                             break
                    if not colision_no_seleccionado:
                        lista_circulos_seleccionados = pygame.sprite.Group.sprites(grupo_circulos_seleccionados)
                        if len(lista_circulos_seleccionados) > 1:
                            # Calcular la posición relativa del puntero del ratón respecto a la esquina superior izquierda del
                            # anteúltimo círculo seleccionado.
                            desp_x = posicion_raton[0] - lista_circulos_seleccionados[-2].rect.x
                            desp_y = posicion_raton[1] - lista_circulos_seleccionados[-2].rect.y
                            # Verificar si el puntero está dentro de la máscara del círculo.
                            if lista_circulos_seleccionados[-2].rect.collidepoint(posicion_raton):
                                if lista_circulos_seleccionados[-2].mask. get_at((desp_x, desp_y)):
                                    canal2.play(sonido_deshacer)
                                    # Mostrar la imagen de círculo no seleccionado para el último círculo seleccionado, eliminar éste
                                    # del grupo de círculos seleccionados, eliminar la última línea de conexión y borrar la última letra
                                    # de la palabra a intentar que coincida con una de las que hay que adivinar.
                                    lista_circulos_seleccionados[-1].deshacer()
                                    grupo_circulos_seleccionados.remove(lista_circulos_seleccionados[-1])
                                    lista_lineas.remove(lista_lineas[-1])
                                    palabra = palabra[:-1]
                                    # Indicar que el anteúltimo círculo seleccionado es el inicio de la conexión en curso, poner el
                                    # puntero del ratón en su centro y a este último como inicio de la línea de conexión.
                                    circulo_inicio=lista_circulos_seleccionados[-2]
                                    pygame.mouse.set_pos(circulo_inicio.rect.center[0], circulo_inicio.rect.center[1])
                                    inicio_linea = (circulo_inicio.rect.center[0], circulo_inicio.rect.center[1])
                    else:
                        colision_no_seleccionado = False            
        # Actualizar sprites.
        grupo_circulos.update()
        # Imagen de fondo del nivel 1.
        ventana.blit(fondo_1, (0, 0))
        # Dibujar el círculo que contendrá las letras.
        pygame.draw.circle(ventana,(250, 240, 230), (ancho_pantalla // 2 + 440, alto_pantalla // 2 + 20), 125)
        # Dibujar las líneas que conectan círculos en una misma conexión de círculos.
        for linea in range(len(lista_lineas)):
            if lista_lineas[linea][1] == lista_lineas[linea][3]:
                grosor = 10
            else:
                grosor = 13
            pygame.draw.line(ventana, (164, 19, 47), (lista_lineas[linea][0], lista_lineas[linea][1]),
                                                     (lista_lineas[linea][2], lista_lineas[linea][3]),
                                                      grosor)
        # Dibujar botón mezclar.
        grupo_boton_mezclar.draw(ventana)
        # Dibujar la línea de la conexión en curso.
        if len(grupo_circulos_seleccionados) > 0:
            pygame.draw.line(ventana, (164, 19, 47), inicio_linea, evento.pos, 12)
        # Dibujar los círculos.
        grupo_circulos.draw(ventana)
        # Poner las letras de la palabra en los círculos.
        for num_circulo in range(len(circulos)):
            if circulos[num_circulo][0] > nivel - 1 and circulos[num_circulo][0] < nivel + 1:
                if circulos[num_circulo][2] not in grupo_circulos_seleccionados:
                    circulos[num_circulo][2].deshacer()
                    texto_letra = arial_65_negrita.render(circulos[num_circulo][3], 0, (60, 66, 107))
                else:
                    texto_letra = arial_65_negrita.render(circulos[num_circulo][3], 0, (255, 255, 255))
                    texto_palabra = arial_40_negrita.render(palabra, 0, (255, 255, 255))
                    ventana.blit(texto_palabra, (ancho_pantalla // 2 + 400, alto_pantalla // 2 - 150))
                ventana.blit(texto_letra, (circulos[num_circulo][6], circulos[num_circulo][7]))
        pygame.display.flip()
    pygame.quit()

Como se observa, cuando se presiona el botón izquierdo del ratón (evento 'MOUSEBUTTONDOWN'), se comprueba si se ha realizado clic sobre la máscara (ver tutoriales sobre colisiones precisas: 'Python + Pygame (XXXI): Tutorial - Colisiones precisas (I)' y 'Python + Pygame (XXXII): Tutorial - Colisiones precisas (II)') de alguno de los círculos en los que se inscriben las letras (estos círculos, todavía no seleccionados, no se ven porque su color es el mismo que el del círculo grande que contiene a todos ellos). Si es así, se selecciona el círculo sobre el que se ha hecho clic, es decir, se incluye éste en el grupo de círculos seleccionados, se muestra la imágen correspondiente al círculo seleccionado y se pone la letra inscrita en el mismo como primera de la palabra que se va a intentar que coincida con una de las que hay que adivinar.

Además, se pone el círculo sobre el que se ha hecho clic como el primero de las conexiones en curso, el puntero del ratón en el centro del círculo y a este último como inicio de la línea de conexión.

En el caso de que no se haya realizado clic sobre la máscara de alguno de los círculos, se comprueba si el clic se ha realizado sobre el icono de barajar las letras, en cuyo caso se indica que se debe barajar.

Cuando se libera o suelta el botón izquierdo del ratón (evento 'MOUSEBUTTONUP'), si se estaba seleccionando círculos, se eliminan todos los círculos seleccionados hasta el momento del grupo correspondiente a los mismos y todas las líneas de conexión entre ellos, y en el caso de haberse completado una palabra se comprueba si es una de las correctas (tal y como se ha indicado, esto no es objeto de este tutorial, mostrándose en el código una sentencia 'print' donde iría el tratamiento para ésto). Además, se borra la palabra que se estaba o ha formando; mientras que si no se estaba seleccionando círculos y el clic se ha hecho sobre el icono de barajar, se cambiará aleatoriamente la posición de las letras, para lo que se utiliza el método 'shuffle' conjuntamente con el método copy (se obtiene una copia de la lista de círculos, que es la que relamente se baraja, y se actualiza la lista original con los datos que nos interesan).

En lo que respecta al seguimiento del movimiento del ratón (evento 'MOUSEMOTION'), si se está seleccionando círculos en los que se inscriben las letras, si el puntero del ratón colisiona con algún círculo que no esté previamente seleccionado, se  incluye dicho círculo en el grupo de círculos seleccionados, se muestra la imagen correspondiente a la selección de círculos y se pone la letra inscrita en el círculo como siguiente de la palabra que se va a intentar que coincida con una de las que hay que adivinar.

Además, se incluye la línea de conexión en la lista de líneas de conexión, se pone al círculo seleccionado como el de inicio de la conexión en curso, al puntero del ratón en el centro del círculo seleccionado y a este último como inicio de la conexión en curso (inicio de la siguiente línea de conexión).

Mientras que si no hay colisión con algún círculo no previamente seleccionado, se comprueba si la colisión se produce con el anteúltimo círculo seleccionado y, si es el caso se procede a deshacer o eliminar la última conexión: mostrar la imagen de círculo no seleccionado para el último círculo seleccionado, eliminar éste del grupo de círculos seleccionados, eliminar la última línea de conexión y borrar la última letra  de la palabra a intentar que coincida con una de las que hay que adivinar. Además, se pone al anteúltimo círculo seleccionado como el de inicio de la conexión en curso, al puntero del ratón en su centro y a este último como inicio de la siguiente línea de conexión.

4.- Clases:

class Boton_Mezclar(pygame.sprite.Sprite):
    # El botón que permite barajar las letras estará definido por la clase Boton_Mezclar, que hereda de la clase sprite.Sprite de Pygame.
    # Clase base para objetos visibles del juego. 
    def __init__(self, x, y):
        super().__init__()
        self.x, self.y  = x, y
        self.image = pygame.image.load('recursos/imagenes/sprites/boton_mezclar.png').convert_alpha()     
        self.rect = self.image.get_rect(center=(self.x, self.y))

    def barajar(self, circulos):
        # Método que permite mezclar o barajar las letras.
        self.circulos = circulos
        shuffle(self.circulos)


class Circulo(pygame.sprite.Sprite):
    # Los círculos en los que están inscritas las letras que forman parte de la palabra estarán definidos por la clase Circulo, que hereda
    # de la clase sprite.Sprite de Pygame. Clase base para objetos visibles del juego.
    def __init__(self, x, y):
        super().__init__()
        self.x, self.y  = x, y
        self.image = pygame.image.load('recursos/imagenes/sprites/circulo_letra_no_seleccionada.png').convert_alpha()
        self.rect = self.image.get_rect(center=(self.x, self.y))
        self.mask = pygame.mask.from_surface(self.image)
        
    def seleccionar(self):
        # Método que muestra la imagen del círculo seleccionado.
        self.image = pygame.image.load('recursos/imagenes/sprites/circulo_letra_seleccionada.png').convert_alpha()

    def deshacer(self):
        # Método que muestra la imagen del círculo no seleccionado.
        self.image = pygame.image.load('recursos/imagenes/sprites/circulo_letra_no_seleccionada.png').convert_alpha()

    def update(self):
        # El método update() se ejecutará en cada frame, y permite actualizar la posición de los círculos.
        self.rect = self.image.get_rect(center=(self.x, self.y))

Al final del contructor de la clase 'Circulo' se puede observar la creación del atributo mask de la imagen del sprite, correspondiente al círculo que se instancia, mediante el método pygame.mask.from_surface(surface)

5.- Poniendo todo junto:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# CONECTOR DE LETRAS:
#
# Tipo juego Words of Wonders (WoW).
#
# Autor: Mikel García Larragan.
# https://mikelgarcialarragan.blogspot.com/

import pygame
import os
from random import shuffle


class Boton_Mezclar(pygame.sprite.Sprite):
    # El botón que permite barajar las letras estará definido por la clase Boton_Mezclar, que hereda de la clase sprite.Sprite de Pygame.
    # Clase base para objetos visibles del juego. 
    def __init__(self, x, y):
        super().__init__()
        self.x, self.y  = x, y
        self.image = pygame.image.load('recursos/imagenes/sprites/boton_mezclar.png').convert_alpha()     
        self.rect = self.image.get_rect(center=(self.x, self.y))

    def barajar(self, circulos):
        # Método que permite mezclar o barajar las letras.
        self.circulos = circulos
        shuffle(self.circulos)


class Circulo(pygame.sprite.Sprite):
    # Los círculos en los que están inscritas las letras que forman parte de la palabra estarán definidos por la clase Circulo, que hereda
    # de la clase sprite.Sprite de Pygame. Clase base para objetos visibles del juego.
    def __init__(self, x, y):
        super().__init__()
        self.x, self.y  = x, y
        self.image = pygame.image.load('recursos/imagenes/sprites/circulo_letra_no_seleccionada.png').convert_alpha()
        self.rect = self.image.get_rect(center=(self.x, self.y))
        self.mask = pygame.mask.from_surface(self.image)
        
    def seleccionar(self):
        # Método que muestra la imagen del círculo seleccionado.
        self.image = pygame.image.load('recursos/imagenes/sprites/circulo_letra_seleccionada.png').convert_alpha()

    def deshacer(self):
        # Método que muestra la imagen del círculo no seleccionado.
        self.image = pygame.image.load('recursos/imagenes/sprites/circulo_letra_no_seleccionada.png').convert_alpha()

    def update(self):
        # El método update() se ejecutará en cada frame, y permite actualizar la posición de los círculos.
        self.rect = self.image.get_rect(center=(self.x, self.y))


def conector_letras():
    # Inicializar módulos internos de pygame.
    pygame.init()
    # Establecer tamaño de la ventana.
    ancho_pantalla, alto_pantalla = 1248, 702
    # 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('Conector de letras')
    # Cargar la imagen del fondo del nivel 1 y cambiar el formato de píxel para crear una copia que se dibujará más rápidamente en la
    # pantalla.
    fondo_1 = pygame.image.load('recursos/imagenes/fondo_1.png')
    fondo_1 = fondo_1.convert()
    # Fuentes.
    arial_40_negrita = pygame.font.SysFont('arial', 40, bold=True)
    arial_65_negrita = pygame.font.SysFont('arial', 65, bold=True)
    # Cargar archivos de sonidos.
    sonido_musica_fondo = pygame.mixer.Sound('recursos/sonidos/musica_fondo.wav')
    sonido_seleccionar = pygame.mixer.Sound('recursos/sonidos/seleccionar.wav')
    sonido_deshacer = pygame.mixer.Sound('recursos/sonidos/deshacer.wav')
    sonido_barajar = pygame.mixer.Sound('recursos/sonidos/barajar.wav')
    # Nivel del juego.
    nivel = 1
    # Crear canales para sonidos simultáneos.
    canal1 = pygame.mixer.Channel(0)
    canal2 = pygame.mixer.Channel(1)
    # Grupo individual para el botón que permite barajar las letras.
    grupo_boton_mezclar = pygame.sprite.GroupSingle()
    boton_mezclar = Boton_Mezclar(ancho_pantalla // 2 + 440, alto_pantalla // 2 + 20)
    grupo_boton_mezclar.add(boton_mezclar)
    # Círculos de las letras de las palabras a adivinar (nivel del juego, número de círculo, sprite del círculo, letra, coordenadas 'x' e 'y'
    # absolutas del círculo, y coordenadas 'x' e 'y' de la letra relativas al centro del círculo.
    circulos = [(1, 1, None, 'A', ancho_pantalla // 2 + 435, alto_pantalla // 2 - 60, None, None),
                (1, 2, None, 'I', ancho_pantalla // 2 + 508, alto_pantalla // 2 + 65, None, None),
                (1, 3, None, 'D', ancho_pantalla // 2 + 372, alto_pantalla // 2 + 65, None, None)]
    # Grupo de sprites. Agrupamos los círculos donde se inscriben las letras que forman parte de la palabra usando la clase sprite.Group de
    # Pygame para hacer que dicha clase se encargue de actualizarlos y dibujarlos, sin necesidad de hacerlo manualmente.
    grupo_circulos = pygame.sprite.Group()
    for circulo_letra in range(len(circulos)):
        if circulos[circulo_letra][0] > nivel - 1 and circulos[circulo_letra][0] < nivel + 1:
            circulo = Circulo(circulos[circulo_letra][4], circulos[circulo_letra][5])
            lista_tupla_circulos = list(circulos[circulo_letra])
            lista_tupla_circulos[2] = circulo
            if lista_tupla_circulos[3] == 'I':
                lista_tupla_circulos[6] = circulo.rect.center[0] - 8
            else:
                lista_tupla_circulos[6] = circulo.rect.center[0] - 18
            lista_tupla_circulos[7] = circulo.rect.center[1] - 40
            circulos[circulo_letra] = tuple(lista_tupla_circulos)
            grupo_circulos.add(circulo)
    # Grupo de sprites. Agrupamos los círculos que han sido seleccionados usando la clase sprite.Group de Pygame para hacer que dicha clase
    # se encargue de actualizarlos y dibujarlos, sin necesidad de hacerlo manualmente.
    grupo_circulos_seleccionados = pygame.sprite.Group()
    # Lista para las líneas que van conectando círculos en una misma conexión.
    lista_lineas = []
    # Palabra que se va a intentar como palabra a adivinar. 
    palabra = ''
    # Variable booleana que indica si se ha producido una colision con un círculo que no ha sido previamente seleccionado.
    colision_no_seleccionado = False
    # Variable booleana que indica si hay que barajar las letras.
    barajar = False
    # Hacer que suene la música de fondo del juego.
    canal1.play(sonido_musica_fondo, -1)
    # Reloj para ejecutar el videojuego.
    reloj = pygame.time.Clock()
    # El juego se ejecutará a 60 frames por segundo.
    fps = 60
    while True:
        # Ejecutar el siguiente frame.
        reloj.tick(fps)
        # Escuchar cada uno de los 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:
                    if len(grupo_circulos_seleccionados) == 0:
                        # 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 alguno de los circulos.
                        for circulo in range(len(circulos)):
                            desp_x =posicion_raton[0] - circulos[circulo][2].rect.x
                            desp_y =posicion_raton[1] - circulos[circulo][2].rect.y
                            # Verificar si el puntero está dentro de la máscara del círculo.
                            if circulos[circulo][2].rect.collidepoint(posicion_raton):
                                if circulos[circulo][2] not in grupo_circulos_seleccionados:
                                    if circulos[circulo][2].mask.get_at((desp_x, desp_y)):           
                                        canal2.play(sonido_seleccionar)
                                        # Incluir el círculo seleccionado en el grupo de círculos seleccionados, mostrar la imagen 
                                        # correspondiente a la selección de círculos y poner la letra como primera de la palabra que se va
                                        # a intentar que coincida con una de las que hay que adivinar.
                                        grupo_circulos_seleccionados.add(circulos[circulo][2])
                                        circulos[circulo][2].seleccionar()
                                        palabra = palabra + circulos [circulo][3]
                                        # Indicar que el círculo sobre el que se ha hecho click es el inicio de la conexión en curso, poner
                                        # el puntero del ratón en su centro y a este último como inicio de la línea de conexión.
                                        circulo_inicio = circulos[circulo][2]
                                        pygame.mouse.set_pos(circulo_inicio.rect.center[0], circulo_inicio.rect.center[1])
                                        inicio_linea = (circulo_inicio.rect.center[0], circulo_inicio.rect.center[1])
                                break
                    if len(grupo_circulos_seleccionados) == 0:
                        # Comprobar si se debe barajar.
                        if boton_mezclar.rect.collidepoint(posicion_raton) and not barajar:
                            barajar = True
            # Comprobar si se ha soltado (liberado) el botón izquierdo del ratón.
            elif evento.type == pygame.MOUSEBUTTONUP:
                if evento.button == 1:
                    if len(grupo_circulos_seleccionados) > 0:
                        # Eliminar los círculos seleccionados del grupo de círculos seleccionados y todas las líneas de conexión.
                        grupo_circulos_seleccionados.empty()
                        lista_lineas =[]
                        # Comprobar si se ha completado una palabra.
                        if len(palabra) == len(grupo_circulos):
                            print('*** Comprobar la palabra formada.')
                        # Borrar el contenido de la palabra a intentar que coincida con una de las que hay que adivinar. 
                        palabra = ''
                    else:
                        while barajar:
                            # Hacer una copia de las lista de circulos. Esta lista es la que será barajada.
                            circulos_barajados = circulos.copy()
                            # Barajar la lista de circulos_barajados (copia de la lista círculos).
                            boton_mezclar.barajar(circulos_barajados)
                            for circulo_barajado in range(len(boton_mezclar.circulos)):
                                if boton_mezclar.circulos[circulo_barajado][3] != circulos[circulo_barajado][3]:
                                    canal2.play(sonido_barajar)
                                    barajar = False
                                    break
                            if not barajar:
                                # Modificar las letras en los círculos conforme a lo obtenido al barajar.
                                for circulo_barajado in range(len(boton_mezclar.circulos)):
                                    lista_tupla_circulos = list(circulos[circulo_barajado])
                                    lista_tupla_circulos[3] = boton_mezclar.circulos[circulo_barajado][3]
                                    if lista_tupla_circulos[3] == 'I':
                                       lista_tupla_circulos[6] = lista_tupla_circulos[2].rect.center[0] - 8
                                    else:
                                       lista_tupla_circulos[6] = lista_tupla_circulos[2].rect.center[0] - 18
                                    lista_tupla_circulos[7] = lista_tupla_circulos[2].rect.center[1] - 40
                                    circulos[circulo_barajado] = tuple(lista_tupla_circulos)
            # Seguimiento del movimiento del ratón.
            elif evento.type == pygame.MOUSEMOTION:
                if len(grupo_circulos_seleccionados) > 0:
                    # Obtener posición del puntero del ratón.
                    posicion_raton = pygame.mouse.get_pos()
                    # Comprobar si el puntero del ratón colisiona con algún círculo que no esté previamente seleccionado.
                    for circulo in range(len(circulos)):
                         desp_x = posicion_raton[0] - circulos[circulo][2].rect.x
                         desp_y = posicion_raton[1] - circulos[circulo][2].rect.y
                         # Verificar si el puntero está dentro de la máscara del círculo.
                         if circulos[circulo][2].rect.collidepoint(posicion_raton):
                             if circulos[circulo][2] not in grupo_circulos_seleccionados:
                                 if circulos[circulo][2].mask.get_at((desp_x, desp_y)):
                                     canal2.play(sonido_seleccionar)
                                     # colision con un círculo que no ha sido previamente seleccionado.
                                     colision_no_seleccionado = True
                                     # Incluir el círculo seleccionado en el grupo de círculos seleccionados, mostrar la imagen
                                     # correspondiente a la selección de círculos y poner la letra como siguiente de la palabra
                                     # que se va a intentar que coincida con una de las que hay que adivinar.
                                     grupo_circulos_seleccionados.add(circulos[circulo][2])
                                     circulos[circulo][2].seleccionar()
                                     palabra = palabra + circulos [circulo][3]
                                     # Incluir la línea de conexión en la lista de líneas de conexión.
                                     lista_lineas.append((circulo_inicio.rect.center[0], circulo_inicio.rect.center[1],
                                                          circulos[circulo][2].rect.center[0], circulos[circulo][2].rect.center[1]))
                                     # Indicar que el círculo seleccionado es el inicio de la conexión en curso, poner el puntero del
                                     # ratón en su centro y a este último como inicio de la línea de conexión.
                                     circulo_inicio = circulos[circulo][2]
                                     pygame.mouse.set_pos(circulo_inicio.rect.center[0], circulo_inicio.rect.center[1])
                                     inicio_linea = (circulo_inicio.rect.center[0], circulo_inicio.rect.center[1])
                             break
                    if not colision_no_seleccionado:
                        lista_circulos_seleccionados = pygame.sprite.Group.sprites(grupo_circulos_seleccionados)
                        if len(lista_circulos_seleccionados) > 1:
                            # Calcular la posición relativa del puntero del ratón respecto a la esquina superior izquierda del
                            # anteúltimo círculo seleccionado.
                            desp_x = posicion_raton[0] - lista_circulos_seleccionados[-2].rect.x
                            desp_y = posicion_raton[1] - lista_circulos_seleccionados[-2].rect.y
                            # Verificar si el puntero está dentro de la máscara del círculo.
                            if lista_circulos_seleccionados[-2].rect.collidepoint(posicion_raton):
                                if lista_circulos_seleccionados[-2].mask. get_at((desp_x, desp_y)):
                                    canal2.play(sonido_deshacer)
                                    # Mostrar la imagen de círculo no seleccionado para el último círculo seleccionado, eliminar éste
                                    # del grupo de círculos seleccionados, eliminar la última línea de conexión y borrar la última letra
                                    # de la palabra a intentar que coincida con una de las que hay que adivinar.
                                    lista_circulos_seleccionados[-1].deshacer()
                                    grupo_circulos_seleccionados.remove(lista_circulos_seleccionados[-1])
                                    lista_lineas.remove(lista_lineas[-1])
                                    palabra = palabra[:-1]
                                    # Indicar que el anteúltimo círculo seleccionado es el inicio de la conexión en curso, poner el
                                    # puntero del ratón en su centro y a este último como inicio de la línea de conexión.
                                    circulo_inicio=lista_circulos_seleccionados[-2]
                                    pygame.mouse.set_pos(circulo_inicio.rect.center[0], circulo_inicio.rect.center[1])
                                    inicio_linea = (circulo_inicio.rect.center[0], circulo_inicio.rect.center[1])
                    else:
                        colision_no_seleccionado = False            
        # Actualizar sprites.
        grupo_circulos.update()
        # Imagen de fondo del nivel 1.
        ventana.blit(fondo_1, (0, 0))
        # Dibujar el círculo que contendrá las letras.
        pygame.draw.circle(ventana,(250, 240, 230), (ancho_pantalla // 2 + 440, alto_pantalla // 2 + 20), 125)
        # Dibujar las líneas que conectan círculos en una misma conexión de círculos.
        for linea in range(len(lista_lineas)):
            if lista_lineas[linea][1] == lista_lineas[linea][3]:
                grosor = 10
            else:
                grosor = 13
            pygame.draw.line(ventana, (164, 19, 47), (lista_lineas[linea][0], lista_lineas[linea][1]),
                                                     (lista_lineas[linea][2], lista_lineas[linea][3]),
                                                      grosor)
        # Dibujar botón mezclar.
        grupo_boton_mezclar.draw(ventana)
        # Dibujar la línea de la conexión en curso.
        if len(grupo_circulos_seleccionados) > 0:
            pygame.draw.line(ventana, (164, 19, 47), inicio_linea, evento.pos, 12)
        # Dibujar los círculos.
        grupo_circulos.draw(ventana)
        # Poner las letras de la palabra en los círculos.
        for num_circulo in range(len(circulos)):
            if circulos[num_circulo][0] > nivel - 1 and circulos[num_circulo][0] < nivel + 1:
                if circulos[num_circulo][2] not in grupo_circulos_seleccionados:
                    circulos[num_circulo][2].deshacer()
                    texto_letra = arial_65_negrita.render(circulos[num_circulo][3], 0, (60, 66, 107))
                else:
                    texto_letra = arial_65_negrita.render(circulos[num_circulo][3], 0, (255, 255, 255))
                    texto_palabra = arial_40_negrita.render(palabra, 0, (255, 255, 255))
                    ventana.blit(texto_palabra, (ancho_pantalla // 2 + 400, alto_pantalla // 2 - 150))
                ventana.blit(texto_letra, (circulos[num_circulo][6], circulos[num_circulo][7]))
        pygame.display.flip()
    pygame.quit()


if __name__ == '__main__':
    conector_letras()

Si lo ejecutamos:

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

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

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