Creació de jocs amb PyBadge

Programació Dades pràctiques     Recursos CITCEA
Tutorial Exemples Projectes   Inici

El joc de les fletxes

En aquest joc, unes fletxes van baixant per la pantalla i cal capturar-les, prement el botó que correspon a cada una, quan estan damunt del rectangle fosc. El joc fa servir cinc botons:

Polsador Funció
START Inici de la partida
Captura les fletxes que apunten a l'esquerra
Captura les fletxes que apunten amunt
Captura les fletxes que apunten a la dreta
Captura les fletxes que apunten avall

Pantalla del joc

Es fan servir dos fitxers de gràfics: tile.bmp i tile2.bmp.

Gràfics         Gràfics

El programa és el següent:

import ugame
import stage
import random
import time
import neopixel
import board
# --- VARIABLES DE ESTADO Y PUNTUACIÓN GLOBALES ---
# Banderas para dibujar las pantallas de UI solo una vez (optimización y limpieza)
pantalla_inici_dibujada = False
pantalla_gameover_dibujada = False
# Récord de la sesión
record_sesion = 0
# --- CONSTANTES DE ESTADO DE JUEGO ---
ESTADO_INICIO = 0
ESTADO_JUEGO = 1
ESTADO_GAMEOVER = 2 # Nuevo estado para forzar la limpieza de pantalla
# Vidas
vidas = 3
vidas_max = 3
# Variables de estado inicial
estado_juego = ESTADO_INICIO
# --- CONFIGURACIÓN DE PANTALLA ---
# Cargar el banco de imágenes
banc = stage.Bank.from_bmp16("tile.bmp")
banc_vidas = stage.Bank.from_bmp16("tile2.bmp")
# Crear el fondo
fons = stage.Grid(banc, 10, 8)
# Crear los cuatro jugadores (flechas)
abajo = stage.Sprite(banc, 7, 32, 10)
arriba = stage.Sprite(banc, 6, 64, 10)
derecha = stage.Sprite(banc, 8, 96, 10)
izquierda = stage.Sprite(banc, 9, 128, 10)
# Vidas
marcador_vidas = stage.Grid(banc_vidas, vidas_max, 1)  # Grid de 1 fila x 3 columnas
marcador_vidas.move(110, 3)  # posición arriba a la izquierda
# El ancho del bloque sigue siendo 6 para evitar la acumulación de números.
texto = stage.Text(width=6, height=1)
# Texto para la pantalla de inicio (ocupa más espacio)
texto_inicio = stage.Text(width=20, height=10)
# Texto para la pantalla de Game Over
texto_gameover = stage.Text(width=20, height=9)
# Crear el juego
joc = stage.Stage(ugame.display, 12)
# --- FUNCIÓN DE INICIALIZACIÓN/REINICIO DEL JUEGO ---
def inicializar_juego():
    global punts, esperando_abajo, esperando_arriba, esperando_derecha, esperando_izquierda
    global temps_actual, temps_reaparicio_abajo, temps_reaparicio_arriba
    global temps_reaparicio_derecha, temps_reaparicio_izquierda
    global velocitat_abajo, velocitat_arriba, velocitat_derecha, velocitat_izquierda
    global tiempo_ultimo_incremento, flechas_perdidas
    global vidas
    # 1. Resetear puntuación y contadores
    punts = 0
    flechas_perdidas = 0
    tiempo_ultimo_incremento = time.monotonic()
    # 2. Resetear estados y posiciones de flechas
    esperando_abajo = True
    esperando_arriba = True
    esperando_derecha = True
    esperando_izquierda = True
    abajo.move(-32, -32)
    arriba.move(-32, -32)
    derecha.move(-32, -32)
    izquierda.move(-32, -32)
    # 3. Resetear velocidades a la base
    velocitat_abajo = 4
    velocitat_arriba = 4
    velocitat_derecha = 4
    velocitat_izquierda = 4
    # 4. Resetear tiempos de reaparición (escalonados)
    temps_actual = time.monotonic()
    temps_reaparicio_abajo = temps_actual + random.randint(1, 3)
    temps_reaparicio_arriba = temps_actual + random.randint(4, 6)
    temps_reaparicio_derecha = temps_actual + random.randint(7, 9)
    temps_reaparicio_izquierda = temps_actual + random.randint(10, 12)
    #5. Resetear marcador de vidas
    vidas = 3  # o el número de vidas que quieras al iniciar
    actualizar_marcador()  # actualiza los corazones al inicio
# --- CONFIGURACIÓN DEL MARCADOR ---
# MARCADOR DE PUNTOS
punts = 0
pos_x = 5
pos_y = 5
texto.move(pos_x, pos_y)
texto.text("{:<6}".format(punts))
# MARCADOR VIDAS
def actualizar_marcador():
    for i in range(vidas_max):
        if i < vidas:
            marcador_vidas.tile(i, 0, 0)  # corazón lleno
        else:
            marcador_vidas.tile(i, 0, 1)  # corazón vacío
# --- CONFIGURACIÓN NEOPIXEL ---
pixel_pin = board.D8
num_pixels = 1
pixels = neopixel.NeoPixel(pixel_pin, num_pixels, brightness=0.3, auto_write=False)
def encender_neopixel(color=(0, 255, 0), duracion=0.2):
    pixels[0] = color
    pixels.show()
    pixels[0] = (0, 0, 0)
    pixels.show()
# --- FUNCIONES AUXILIARES ---
def dib_taulell():
    # Dibuja el fondo del tablero de juego (césped, carretera, etc.)
    tau = [
        [11, 11, 11, 11, 11, 11, 11, 11, 11, 11],
        [11, 11, 11, 11, 11, 11, 11, 11, 11, 11],
        [12, 12, 12, 12, 12, 12, 12, 12, 12, 12],
        [12, 12, 12, 12, 12, 12, 12, 12, 12, 12],
        [13, 13, 13, 13, 13, 13, 13, 13, 13, 13],
        [13, 0, 4, 4, 4, 4, 4, 4, 1, 13],
        [14, 3, 5, 5, 5, 5, 5, 5, 2, 14],
        [14, 14, 14, 14, 14, 14, 14, 14, 14, 14]
    ]
    for i in range(8):
        for j in range(10):
            fons.tile(j, i, tile=tau[i][j])
def dib_fons_inici():
    # Dibuja un fondo simple y uniforme para la pantalla de inicio
    # Usamos el tile 11 (parece cielo o un color liso)
    for i in range(8):
        for j in range(10):
            fons.tile(j, i, tile=11)
def obtener_x_libre(flechas_activas):
    # Lógica para obtener una posición X libre
    posibles_x = [16, 32, 48, 64, 80, 96, 112, 128]  # 8 columnas
    x_ocupadas = [flecha.x for flecha in flechas_activas if flecha.x > 0] # Aseguramos que solo contamos flechas visibles
    libres = [x for x in posibles_x if x not in x_ocupadas]

    if not libres:
        return random.choice(posibles_x)
    return random.choice(libres)
# Hacemos el render_block inicial para dibujar el fondo
joc.render_block()
# --- BUCLE PRINCIPAL DEL JUEGO ---
while True:
    if estado_juego == ESTADO_INICIO:
        keys = ugame.buttons.get_pressed()
        if not pantalla_inici_dibujada:
            # Bloque de limpieza de texto completo para el objeto 20x10
            # Se usa 10 líneas de 20 espacios (200 caracteres)
            bloque_limpieza = (" " * 20 + "\n") * 9 + (" " * 20)
            dib_fons_inici()
            # --- LIMPIEZA DEFENSA ACTIVA ---
            # Aseguramos que el texto del marcador esté fuera.
            texto.text("")
            texto.move(-32, -32)
            # Limpieza agresiva del texto de Game Over para evitar artefactos
            texto_gameover.text(bloque_limpieza)
            texto_gameover.move(-32, -32)
            # -------------------------------
            texto_inicio.move(3, 3)
            texto_inicio.text(
            " \n\n"
                "    EL JUEGO DE \n"
                "    LAS FLECHAS\n\n\n"
                "  ¡Pulsa el boton\n"
                "     START para\n"
                "      empezar!"
            )
            joc.layers = [ texto_inicio, fons]
            joc.render_block()
            pantalla_inici_dibujada = True
        joc.tick()
        if keys & ugame.K_START:
            pantalla_inici_dibujada = False    # Resetear bandera de inicio
            pantalla_gameover_dibujada = False # Resetear bandera de game over
            # Es crucial mover el texto de inicio fuera de la pantalla antes de empezar
            texto_inicio.text("")
            texto_inicio.move(-32, -32)
            inicializar_juego()
            dib_taulell()
            # --- Restaurar el marcador de puntos para la nueva partida ---
            pos_x = 1
            pos_y = 5
            texto.move(pos_x, pos_y)
            texto.text("{:<6}".format(punts))
            # ------------------------------------------------------------
            estado_juego = ESTADO_JUEGO
            # Se establece el orden de las capas: Texto (puntuación) sobre las flechas, flechas sobre el fondo.
            joc.layers = [texto,marcador_vidas, abajo, arriba, derecha, izquierda, fons]
            joc.render_block()
    # --- PANTALLA DE GAME OVER (ESTADO_GAMEOVER) ---
    elif estado_juego == ESTADO_GAMEOVER:
        keys = ugame.buttons.get_pressed()
        # Aseguramos que todas las flechas (sprites) estén fuera
        # de la vista, limpiando cualquier artefacto gráfico que pudieran causar.
        abajo.move(-32, -32)
        arriba.move(-32, -32)
        derecha.move(-32, -32)
        izquierda.move(-32, -32)
        joc.render_sprites([]) # Borra cualquier sprite que haya quedado en el buffer
        # Solo dibujamos la pantalla una vez
        if not pantalla_gameover_dibujada:
            # Bloque de limpieza de texto completo para el objeto 20x10
            bloque_limpieza = (" " * 20 + "\n") * 8 + (" " * 20)
            dib_fons_inici() # Dibuja fondo (configura los tiles)
            # Asegurarse de que el récord esté actualizado antes de mostrarlo
            if punts > record_sesion:
                record_sesion = punts
            # Limpiamos y movemos el marcador de puntos fuera de la pantalla
            texto.text("")
            texto.move(-32, -32)
            # CRUCIAL: Limpiamos explícitamente el bloque de texto de Game Over
            # Usamos el bloque_limpieza para sobrescribir todo el área antes de escribir el nuevo texto.
            texto_gameover.text(bloque_limpieza)
            texto_gameover.move(3, 20)
            texto_gameover.text(
                "      GAME OVER\n\n"
                "  Puntos: " + str(punts) + "\n\n"
                "  Record: " + str(record_sesion) + "\n\n"
                "  Pulsa START para\n"
                "     reiniciar\n"
            )
            # Establecemos las capas y renderizamos UNA SOLA VEZ
            joc.layers = [ texto_gameover, fons]
            joc.render_block()
            pantalla_gameover_dibujada = True  # Solo se dibuja una vez
        joc.tick()
        if keys & ugame.K_START:
            # Bloque de limpieza de texto completo para el objeto 20x10
            bloque_limpieza = (" " * 20 + "\n") * 9 + (" " * 20)
            # Limpiamos el texto_gameover y lo movemos fuera de la pantalla
            # Usamos la limpieza agresiva al salir para que esté listo para la siguiente partida
            texto_gameover.text(bloque_limpieza)
            texto_gameover.move(-32, -32)
            # Limpieza adicional: Forzamos un renderizado del fondo solo antes de ir a INICIO
            joc.layers = [fons]
            joc.render_block()
            pantalla_gameover_dibujada = False  # reinicia la bandera
            pantalla_inici_dibujada = False
            estado_juego = ESTADO_INICIO
    # --- BUCLE PRINCIPAL DEL JUEGO (ESTADO_JUEGO) ---
    elif estado_juego == ESTADO_JUEGO:
        actualizar_marcador()
        puntuacion_anterior = punts
        temps_actual = time.monotonic()  # Tiempo actual en segundos
        # --- Lógica de Aceleración y Movimiento de Flechas ---
        interval_incremento = 10
        incremento_velocitat = 1
        # Aumentar velocidades con el tiempo
        if temps_actual - tiempo_ultimo_incremento > interval_incremento:
            velocitat_abajo += incremento_velocitat
            velocitat_arriba += incremento_velocitat
            velocitat_derecha += incremento_velocitat
            velocitat_izquierda += incremento_velocitat
            tiempo_ultimo_incremento = temps_actual
        # --- Obtener lista de flechas activas para la función de aparición ---
        # Si la flecha no está "esperando", está activa en pantalla.
        flechas_activas = []
        if not esperando_abajo: flechas_activas.append(abajo)
        if not esperando_arriba: flechas_activas.append(arriba)
        if not esperando_derecha: flechas_activas.append(derecha)
        if not esperando_izquierda: flechas_activas.append(izquierda)
        # -- jugador "abajo" --
        if not esperando_abajo:
            abajo.move(abajo.x, abajo.y + velocitat_abajo)
            if abajo.y > 138:
                esperando_abajo = True
                flechas_perdidas += 1
                vidas -= 1
                actualizar_marcador()
                joc.render_block()
                encender_neopixel((255, 0, 0)) # Rojo por flecha perdida
                abajo.move(-32, -32)
                temps_reaparicio_abajo = temps_actual + random.randint(1, 3)
                if flechas_perdidas >= 3:
                    if punts > record_sesion: record_sesion = punts # Actualiza récord
                    estado_juego = ESTADO_GAMEOVER # Transición a Game Over
        else:
            if temps_actual >= temps_reaparicio_abajo:
                esperando_abajo = False
                # Aquí la lógica de obtener_x_libre es compleja, usamos solo las flechas activas para determinar la X libre.
                x_reaparecer = obtener_x_libre(flechas_activas)
                abajo.move(x_reaparecer, -16)
                actualizar_marcador()
        # -- jugador "arriba" --
        if not esperando_arriba:
            arriba.move(arriba.x, arriba.y + velocitat_arriba)
            if arriba.y > 138:
                esperando_arriba = True
                flechas_perdidas += 1
                vidas -= 1
                actualizar_marcador()
                joc.render_block()
                encender_neopixel((255, 0, 0))
                arriba.move(-32, -32)
                temps_reaparicio_arriba = temps_actual + random.randint(1, 3)
                if flechas_perdidas >= 3:
                    if punts > record_sesion: record_sesion = punts
                    estado_juego = ESTADO_GAMEOVER # Transición a Game Over
        else:
            if temps_actual >= temps_reaparicio_arriba:
                esperando_arriba = False
                x_reaparecer = obtener_x_libre(flechas_activas)
                arriba.move(x_reaparecer, -16)
                actualizar_marcador()
        # -- jugador "derecha" --
        if not esperando_derecha:
            derecha.move(derecha.x, derecha.y + velocitat_derecha)
            if derecha.y > 138:
                esperando_derecha = True
                flechas_perdidas += 1
                vidas -= 1
                actualizar_marcador()
                joc.render_block()
                encender_neopixel((255, 0, 0))
                derecha.move(-32, -32)
                temps_reaparicio_derecha = temps_actual + random.randint(1, 3)
                if flechas_perdidas >= 3:
                    if punts > record_sesion: record_sesion = punts
                    estado_juego = ESTADO_GAMEOVER # Transición a Game Over
        else:
            if temps_actual >= temps_reaparicio_derecha:
                esperando_derecha = False
                x_reaparecer = obtener_x_libre(flechas_activas)
                derecha.move(x_reaparecer, -16)
                actualizar_marcador()
        # -- jugador "izquierda" --
        if not esperando_izquierda:
            izquierda.move(izquierda.x, izquierda.y + velocitat_izquierda)
            if izquierda.y > 138:
                esperando_izquierda = True
                flechas_perdidas += 1
                vidas -= 1
                actualizar_marcador()
                joc.render_block()
                encender_neopixel((255, 0, 0))
                izquierda.move(-32, -32)
                temps_reaparicio_izquierda = temps_actual + random.randint(1, 3)
                if flechas_perdidas >= 3:
                    if punts > record_sesion: record_sesion = punts
                    estado_juego = ESTADO_GAMEOVER # Transición a Game Over
        else:
            if temps_actual >= temps_reaparicio_izquierda:
                esperando_izquierda = False
                x_reaparecer = obtener_x_libre(flechas_activas)
                izquierda.move(x_reaparecer, -16)
                actualizar_marcador()
        # --- Zona d'impacte ---
        zona_y_min = 80
        zona_y_max = 112
        fletxes_en_zona = {
            "UP": arriba.y >= zona_y_min and arriba.y <= zona_y_max,
            "DOWN": abajo.y >= zona_y_min and abajo.y <= zona_y_max,
            "LEFT": izquierda.y >= zona_y_min and izquierda.y <= zona_y_max,
            "RIGHT": derecha.y >= zona_y_min and derecha.y <= zona_y_max
        }
        # --- Lectura dels polsadors del PyBadge ---
        keys = ugame.buttons.get_pressed()
        # --- Detecció d'encerts ---
        if keys & ugame.K_UP and fletxes_en_zona["UP"]:
            # Lógica de acierto UP...
            punts += 1
            arriba.move(-32, -32)
            esperando_arriba = True
            temps_reaparicio_arriba = temps_actual + random.randint(1, 3)
            encender_neopixel(color=(0, 255, 0))
        if keys & ugame.K_DOWN and fletxes_en_zona["DOWN"]:
            # Lógica de acierto DOWN...
            punts += 1
            abajo.move(-32, -32)
            esperando_abajo = True
            temps_reaparicio_abajo = temps_actual + random.randint(1, 3)
            encender_neopixel(color=(0, 255, 0))
        if keys & ugame.K_LEFT and fletxes_en_zona["LEFT"]:
            # Lógica de acierto LEFT...
            punts += 1
            izquierda.move(-32, -32)
            esperando_izquierda = True
            temps_reaparicio_izquierda = temps_actual + random.randint(1, 3)
            encender_neopixel(color=(0, 255, 0))
        if keys & ugame.K_RIGHT and fletxes_en_zona["RIGHT"]:
            # Lógica de acierto RIGHT...
            punts += 1
            derecha.move(-32, -32)
            esperando_derecha = True
            temps_reaparicio_derecha = temps_actual + random.randint(1, 3)
            encender_neopixel(color=(0, 255, 0))
        # ?? Actualizamos el marcador SOLO si el valor ha cambiado.
        if punts != puntuacion_anterior:
            # Usamos relleno de 6 espacios con justificación izquierda (<)
            texto.text("{:<6}".format(punts))
            # 2. Forzamos el redibujado de las capas estáticas (como el texto)
            joc.render_block()
        # Dibujar sprites activos
        sprites_visibles = []
        if not esperando_abajo:
            sprites_visibles.append(abajo)
        if not esperando_arriba:
            sprites_visibles.append(arriba)
        if not esperando_derecha:
            sprites_visibles.append(derecha)
        if not esperando_izquierda:
            sprites_visibles.append(izquierda)
        joc.render_sprites(sprites_visibles)
        joc.tick()

 

 

 

 

 

 

 

 

 

 

Licencia de Creative Commons
Esta obra de Oriol Boix está licenciada bajo una licencia no importada Creative Commons Reconocimiento-NoComercial-SinObraDerivada 3.0.