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 |

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)

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