Creació de jocs amb PyBadge

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

Fluppy Vird

Aquest joc, recreació del joc clàssic Flappy Bird, consisteix en moure un ocell perquè sigui capaç de passar per les separacions que hi ha entre diverses parells de tubs. El joc fa servir tres botons:

Polsador Funció
A Desplaça l'ocell cap amunt
B Augmenta la velocitat de l'ocell durant un poc temps
SELECT Pausa de la partida

Pantalla del joc

Els gràfics s'han creat a partir de matrius de color i, per tant, no hi ha fitxers de gràfics. La puntuació màxima aconseguida es guarda en la memòria permanent del microcontrolador, de manera que sempre es pot comparar la puntuació assolida en una partida amb la màxima absoluta.

En aquest cas, fan servir un fitxer per guardar la puntuació més alta. Atès que no es pot escriure al directori CIRCUITPY des del programa si està habilitada la connexió des de l'ordinador, cal fer un programa boot.py que ens permeti gestionar qui té permís per accedir a la unitat.

El programa del joc és el següent:

# ===============================
# Llibreries bàsiques i inicialitzacions
# ===============================
import board
import displayio
import ugame
import random
import time
import terminalio
import neopixel
from adafruit_display_text import label
from adafruit_display_shapes.rect import Rect
from adafruit_display_shapes.roundrect import RoundRect
display = board.DISPLAY
display.auto_refresh = False
pixel = neopixel.NeoPixel(board.NEOPIXEL, 1)
pixel.brightness = 0.2  # Intensitat moderada
# ===============================
# Guardat de puntuació més alta
# ===============================
HIGH_SCORE_FILE = "/highscore.txt"
def load_high_score():
    try:
        with open(HIGH_SCORE_FILE, "r") as f:
            return int(f.read())
    except OSError:
        return 0
    except ValueError:
        return 0
def save_high_score(score):
    try:
        with open(HIGH_SCORE_FILE, "w") as f:
            f.write(str(score))
    except OSError as e:
        print("Error saving high score:", e)
high_score = load_high_score()
# ===============================
# Constants del joc
# ===============================
SCREEN_WIDTH = 160
SCREEN_HEIGHT = 128
PIPE_WIDTH = 16
PIPE_SPEED = 2
BOOSTED_PIPE_SPEED = 5
BOOST_DURATION = 10
boost_timer = 0
led_timer = 0
BOOST_COOLDOWN = 120
boost_cooldown = 0
PIPE_GAP = 40
PIPE_MIN_GAP_Y = 30
PIPE_MAX_GAP_Y = SCREEN_HEIGHT - PIPE_GAP - 30
BIRD_WIDTH = 12
BIRD_HEIGHT = 12
MAX_LIVES = 3
INVULNERABILITY_FRAMES = 30
gravity = 0.4
flap_strength = -3.0
PIPE_SPACING = 70
# ===============================
# Colors constants
# ===============================
# Fons
SKY_COLORS = [0x87CEEB, 0x70B5E7, 0x5598D3, 0x3B7BC1, 0x2B65AD, 0x1B4A8A]
GROUND_COLOR = 0x654321
# Pipes
PIPE_BODY_COLOR = 0x0A8A0A
PIPE_CAP_COLOR = 0x12C312
PIPE_SHADOW_COLOR = 0x055005
# Ocells
BIRD_COLOR_PRIMARY = 0xFFAA00
BIRD_COLOR_FLAP = 0xFFDD00
BIRD_COLOR_BOOST = 0xFF5555
BIRD_COLOR_HIGHLIGHT = 0xFFFFFF
BIRD_COLOR_SHADOW = 0x000000
# UI
TEXT_COLOR = 0xFFFFFF
TITLE_COLOR = 0xFFFF00
BOOST_READY_COLOR = 0x00FF00
BOOST_ACTIVE_COLOR = 0xFF5555
BOOST_COOLDOWN_COLOR = 0xAAAAAA
# Menús
MENU_GRADIENT_COLORS = [0x1B4A8A, 0x3B7BC1, 0x5598D3]
# ===============================
# Fons del joc
# ===============================
background_group = displayio.Group()
band_height = SCREEN_HEIGHT // len(SKY_COLORS)
for i, color in enumerate(SKY_COLORS):
    band = Rect(0, i * band_height, SCREEN_WIDTH, band_height, fill=color)
    background_group.append(band)
ground = Rect(0, SCREEN_HEIGHT - 12, SCREEN_WIDTH, 12, fill=GROUND_COLOR)
background_group.append(ground)
# ===============================
# Ocells
# ===============================
bird_bitmap = displayio.Bitmap(BIRD_WIDTH, BIRD_HEIGHT, 4)
bird_palette = displayio.Palette(4)
bird_palette[0] = BIRD_COLOR_SHADOW
bird_palette.make_transparent(0)
bird_palette[1] = BIRD_COLOR_PRIMARY
bird_palette[2] = BIRD_COLOR_FLAP
bird_palette[3] = BIRD_COLOR_HIGHLIGHT
bird_pixels = [
    0,0,0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,1,1,1,0,0,0,0,0,
    0,0,0,1,2,2,2,1,0,0,0,0,
    0,0,1,2,2,3,2,1,0,0,0,0,
    0,1,2,2,3,3,2,1,0,0,0,0,
    0,1,2,2,2,2,2,1,1,0,0,0,
    0,0,1,2,2,2,2,2,1,0,0,0,
    0,0,0,1,2,2,2,1,0,0,0,0,
    0,0,0,1,1,1,1,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,0,0,
]
for i, color in enumerate(bird_pixels):
    x = i % BIRD_WIDTH
    y = i // BIRD_WIDTH
    bird_bitmap[x, y] = color
bird = displayio.TileGrid(bird_bitmap, pixel_shader=bird_palette, x=50, y=60)
# ===============================
# Pipes
# ===============================
def create_pipe(x, gap_y):
    group = displayio.Group()
    top_pipe = RoundRect(x, 0, PIPE_WIDTH, gap_y, 3, fill=PIPE_BODY_COLOR)
    top_cap = RoundRect(x - 5, gap_y - 6, PIPE_WIDTH + 10, 6, 3, fill=PIPE_CAP_COLOR)
    bottom_height = SCREEN_HEIGHT - (gap_y + PIPE_GAP)
    bottom_pipe = RoundRect(x, gap_y + PIPE_GAP, PIPE_WIDTH, bottom_height, 3, fill=PIPE_BODY_COLOR)
    bottom_cap = RoundRect(x - 5, gap_y + PIPE_GAP, PIPE_WIDTH + 10, 6, 3, fill=PIPE_CAP_COLOR)
    top_shadow = Rect(x + PIPE_WIDTH - 3, 0, 3, gap_y, fill=PIPE_SHADOW_COLOR)
    bottom_shadow = Rect(x + PIPE_WIDTH - 3, gap_y + PIPE_GAP, 3, bottom_height, fill=PIPE_SHADOW_COLOR)
    for item in [top_pipe, top_cap, top_shadow, bottom_pipe, bottom_cap, bottom_shadow]:
        group.append(item)
    return group
def update_pipe(pipe):
    x = pipe["x"]
    gap_y = pipe["gap_y"]
    bottom_height = SCREEN_HEIGHT - (gap_y + PIPE_GAP)
    pipe["group"][0] = RoundRect(x, 0, PIPE_WIDTH, gap_y, 3, fill=PIPE_BODY_COLOR)
    pipe["group"][1] = RoundRect(x - 5, gap_y - 6, PIPE_WIDTH + 10, 6, 3, fill=PIPE_CAP_COLOR)
    pipe["group"][2] = Rect(x + PIPE_WIDTH - 3, 0, 3, gap_y, fill=PIPE_SHADOW_COLOR)
    pipe["group"][3] = RoundRect(x, gap_y + PIPE_GAP, PIPE_WIDTH, bottom_height, 3, fill=PIPE_BODY_COLOR)
    pipe["group"][4] = RoundRect(x - 5, gap_y + PIPE_GAP, PIPE_WIDTH + 10, 6, 3, fill=PIPE_CAP_COLOR)
    pipe["group"][5] = Rect(x + PIPE_WIDTH - 3, gap_y + PIPE_GAP, 3, bottom_height, fill=PIPE_SHADOW_COLOR)
pipes = []
for i in range(3):
    x = SCREEN_WIDTH + i * PIPE_SPACING
    gap_y = random.randint(PIPE_MIN_GAP_Y, PIPE_MAX_GAP_Y)
    pipe_group = create_pipe(x, gap_y)
    pipes.append({"group": pipe_group, "x": x, "gap_y": gap_y, "passed": False})

# ===============================
# Interfície
# ===============================
lives_label = label.Label(terminalio.FONT, text="", color=TEXT_COLOR, x=5, y=5)
score_label = label.Label(terminalio.FONT, text="", color=TEXT_COLOR, x=SCREEN_WIDTH - 70, y=5)
boost_label = label.Label(terminalio.FONT, text="Boost Ready", color=BOOST_READY_COLOR, x=SCREEN_WIDTH // 2 - 30, y=SCREEN_HEIGHT - 10)

# Grups
game_group = displayio.Group()
game_group.append(background_group)
game_group.append(bird)
for pipe in pipes:
    game_group.append(pipe["group"])
game_group.append(lives_label)
game_group.append(score_label)
game_group.append(boost_label)

# ===============================
# Menú, pausa i game over
# ===============================
menu_group = displayio.Group()
pause_group = displayio.Group()
game_over_group = displayio.Group()

for group in [menu_group, pause_group, game_over_group]:
    for i, color in enumerate(MENU_GRADIENT_COLORS):
        band = Rect(0, i * (SCREEN_HEIGHT // len(MENU_GRADIENT_COLORS)), SCREEN_WIDTH, SCREEN_HEIGHT // len(MENU_GRADIENT_COLORS), fill=color)
        group.append(band)
    group.append(RoundRect(5, 5, SCREEN_WIDTH - 10, SCREEN_HEIGHT - 10, 5, outline=TITLE_COLOR, stroke=2))

title_label = label.Label(terminalio.FONT, text="FLAPPY BIRD", color=TITLE_COLOR, x=30, y=30)
instr_label = label.Label(terminalio.FONT, text="Press A to Start", color=TEXT_COLOR, x=25, y=SCREEN_HEIGHT - 20)
menu_group.append(title_label)
menu_group.append(instr_label)

pause_label = label.Label(terminalio.FONT, text="Game Paused\nPress A to Resume", color=TEXT_COLOR, x=25, y=SCREEN_HEIGHT // 2)
pause_group.append(pause_label)

game_over_label = label.Label(terminalio.FONT, text="", color=TEXT_COLOR, x=25, y=SCREEN_HEIGHT // 2 - 20)
game_over_group.append(game_over_label)
# ===============================
# Fade overlay
# ===============================
fade_overlay = Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, fill=0x000000)

def fade_out(duration=15):
    display.root_group.append(fade_overlay)
    for i in range(duration):
        brightness = int(255 * (i + 1) / duration)
        gray = (brightness << 16) | (brightness << 8) | brightness
        fade_overlay.fill = gray
        display.refresh(minimum_frames_per_second=0)
        time.sleep(0.03)
    display.root_group.remove(fade_overlay)
def fade_in(duration=15):
    display.root_group.append(fade_overlay)
    for i in range(duration):
        brightness = int(255 * (duration - i - 1) / duration)
        gray = (brightness << 16) | (brightness << 8) | brightness
        fade_overlay.fill = gray
        display.refresh(minimum_frames_per_second=0)
        time.sleep(0.03)
    display.root_group.remove(fade_overlay)
# ===============================
# Variables de joc
# ===============================
bird_x = 50
bird_y = 60
bird_velocity = 0
lives = MAX_LIVES
invuln_counter = 0
score = 0
game_state = "menu"
# ===============================
# Funcions de joc
# ===============================
def update_difficulty(score):
    global  PIPE_GAP, PIPE_SPEED, BOOSTED_PIPE_SPEED
    min_gap = 30           # espai mínim raonable
    max_speed = 6          # velocitat màxima normal
    max_boost_speed = 10   # velocitat màxima amb boost

    PIPE_GAP = int(max(40 - score * 0.25, min_gap))
    PIPE_SPEED = int(min(2 + score * 0.15, max_speed))
    BOOSTED_PIPE_SPEED = int(min(5 + score * 0.2, max_boost_speed))
def reset_game():
    global bird_y, bird_velocity, lives, invuln_counter, score, game_state
    bird_y = 60
    bird_velocity = 0
    lives = MAX_LIVES
    invuln_counter = 0
    score = 0
    update_difficulty(score)
    lives_label.text = f"Lives: {lives}"
    score_label.text = f"Score: {score}"
    for i, pipe in enumerate(pipes):
        pipe["x"] = SCREEN_WIDTH + i * PIPE_SPACING
        pipe["gap_y"] = random.randint(PIPE_MIN_GAP_Y, PIPE_MAX_GAP_Y)
        pipe["passed"] = False
        update_pipe(pipe)
    game_state = "playing"
    display.root_group = game_group
def show_game_over():
    global high_score, game_state
    if score > high_score:
        high_score = score
        save_high_score(high_score)
    game_over_label.text = f"Game Over\nScore: {score}\nHigh Score: {high_score}\nPress A to Restart"
    game_state = "game_over"
    display.root_group = game_over_group
def lose_life():
    global lives, invuln_counter
    lives -= 1
    lives_label.text = f"Lives: {lives}"
    invuln_counter = INVULNERABILITY_FRAMES
    if lives <= 0:
        show_game_over()
# ===============================
# Inicia el menú
# ===============================
display.root_group = menu_group
blink_counter = 0
blink_interval = 15

# ===============================
# Bucle principal
# ===============================
while True:
    keys = ugame.buttons.get_pressed()
    # ---------- MENU ----------
    if game_state == "menu":
        blink_counter = (blink_counter + 1) % (blink_interval * 2)
        instr_label.hidden = blink_counter > blink_interval
        if keys & ugame.K_X:
            fade_out()
            reset_game()
            fade_in()
    # ---------- GAME OVER ----------
    elif game_state == "game_over":
        if keys & ugame.K_X:
            fade_out()
            display.root_group = menu_group
            game_state = "menu"
            fade_in()
    # ---------- PAUSED ----------
    elif game_state == "paused":
        if keys & ugame.K_X:
            fade_out()
            game_state = "playing"
            display.root_group = game_group
            fade_in()
    # ---------- PLAYING ----------
    elif game_state == "playing":
        if keys & ugame.K_SELECT:
            fade_out()
            game_state = "paused"
            display.root_group = pause_group
            fade_in()
        # Flap
        if keys & ugame.K_X:
            bird_velocity = flap_strength
            bird_palette[1] = BIRD_COLOR_FLAP
        else:
            bird_palette[1] = BIRD_COLOR_PRIMARY
        # Gravetat
        bird_velocity += gravity
        bird_y += bird_velocity
        # Límit superior i inferior
        if bird_y < 0:
            bird_y = 0
            bird_velocity = 0
            if invuln_counter == 0:
                lose_life()
        elif bird_y > SCREEN_HEIGHT - BIRD_HEIGHT - 12:
            bird_y = SCREEN_HEIGHT - BIRD_HEIGHT - 12
            bird_velocity = 0
            if invuln_counter == 0:
                lose_life()
        bird.x = bird_x
        bird.y = int(bird_y)
        # Invulnerabilitat
        if invuln_counter > 0:
            invuln_counter -= 1
            bird.hidden = (invuln_counter // 2) % 2 == 0
        else:
            bird.hidden = False
        # Boost
        if keys & ugame.K_O and boost_timer == 0 and boost_cooldown == 0:
            boost_timer = BOOST_DURATION
            boost_cooldown = BOOST_COOLDOWN
        if boost_cooldown > 0:
            boost_cooldown -= 1
        if boost_timer > 0:
            boost_label.text = "Boost Active!"
            boost_label.color = BOOST_ACTIVE_COLOR
        elif boost_cooldown > 0:
            boost_label.text = "Boost Recharging..."
            boost_label.color = BOOST_COOLDOWN_COLOR
        else:
            boost_label.text = "Boost Ready"
            boost_label.color = BOOST_READY_COLOR
        current_pipe_speed = BOOSTED_PIPE_SPEED if boost_timer > 0 else PIPE_SPEED
        if boost_timer > 0:
            boost_timer -= 1
            bird_palette[1] = BIRD_COLOR_BOOST
        else:
            bird_palette[1] = BIRD_COLOR_PRIMARY
        if led_timer > 0:
            led_timer -= 1
            if led_timer == 0:
                pixel.fill((0, 0, 0))
        # Pipes i col·lisions
        for pipe in pipes:
            pipe["x"] -= current_pipe_speed
            if pipe["x"] < -PIPE_WIDTH - 5:
                pipe["x"] = SCREEN_WIDTH
                pipe["gap_y"] = random.randint(PIPE_MIN_GAP_Y, PIPE_MAX_GAP_Y)
                pipe["passed"] = False
                update_pipe(pipe)
            else:
                pipe["group"][0].x = pipe["x"]
                pipe["group"][1].x = pipe["x"] - 5
                pipe["group"][2].x = pipe["x"] + PIPE_WIDTH - 3
                pipe["group"][3].x = pipe["x"]
                pipe["group"][4].x = pipe["x"] - 5
                pipe["group"][5].x = pipe["x"] + PIPE_WIDTH - 3
            top_pipe_rect = (pipe["x"], 0, PIPE_WIDTH, pipe["gap_y"])
            bottom_pipe_rect = (pipe["x"], pipe["gap_y"] + PIPE_GAP, PIPE_WIDTH, SCREEN_HEIGHT - (pipe["gap_y"] + PIPE_GAP))
            top_cap_rect = (pipe["x"] - 5, pipe["gap_y"] - 6, PIPE_WIDTH + 10, 6)
            bottom_cap_rect = (pipe["x"] - 5, pipe["gap_y"] + PIPE_GAP, PIPE_WIDTH + 10, 6)
            bird_rect = (bird.x + 2, bird.y + 2, BIRD_WIDTH - 4, BIRD_HEIGHT - 4)
            def rects_overlap(r1, r2):
                x1, y1, w1, h1 = r1
                x2, y2, w2, h2 = r2
                return not (x1 + w1 < x2 or x1 > x2 + w2 or y1 + h1 < y2 or y1 > y2 + h2)
            if not pipe["passed"] and invuln_counter == 0:
                if (rects_overlap(bird_rect, top_pipe_rect) or
                rects_overlap(bird_rect, bottom_pipe_rect) or
                rects_overlap(bird_rect, top_cap_rect) or
                rects_overlap(bird_rect, bottom_cap_rect)):
                    lose_life()
                    pixel.fill((255, 0, 0))
                    led_timer = 3
                    pipe["passed"] = True
            if not pipe["passed"] and bird.x > pipe["x"] + PIPE_WIDTH  and invuln_counter == 0:
                score += 1
                score_label.text = f"Score: {score}"
                pipe["passed"] = True
                pixel.fill((0, 255, 0))
                led_timer = 3
                update_difficulty(score)
    display.refresh()
    time.sleep(0.03)

 

 

 

 

 

 

 

 

 

 

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