Editor

Flappy Square in Python

Pygame Online · 153 lines · Up or Space to flap

Flap through the gaps between the pipes for as long as you can.

How it works

Gravity in two lines

Every frame gravity adds a little to the downward speed, and the speed moves the square. A flap sets the speed to a fixed upward value. That is the whole physics of the game.

speed_y += GRAVITY
bird_y += speed_y

Pipes are dictionaries

A pipe is just where it is (x), where its gap is (gap_y) and whether you scored it. Each new gap is near the last one, so the course stays possible.

def add_pipe():
    """A new pipe at the right edge, its gap not too far from the last one."""
    if pipes:
        gap_y = pipes[-1]["gap_y"] + random.randint(-90, 90)
    else:
        gap_y = HEIGHT // 2
    gap_y = max(70, min(gap_y, GROUND - 70))
    pipes.append({"x": WIDTH, "gap_y": gap_y, "scored": False})

From a gap to two rectangles

pipe_rects turns a pipe into the top and bottom rectangles, which are used both to draw it and to check for a crash.

def pipe_rects(pipe):
    """The top and bottom parts of a pipe."""
    top = pygame.Rect(pipe["x"], 0, PIPE_WIDTH, pipe["gap_y"] - GAP // 2)
    bottom_y = pipe["gap_y"] + GAP // 2
    bottom = pygame.Rect(pipe["x"], bottom_y, PIPE_WIDTH, GROUND - bottom_y)
    return top, bottom

Make it yours

Change the code above and press Run again. Some ideas:

  • Make the gaps narrower as the score rises.
  • Change GRAVITY and FLAP to make the square floatier or heavier.
  • Add coins between the pipes that are worth an extra point.
  • Draw a proper bird with circles and a triangle for the beak.

The whole program

Plain pygame: it runs the same on a computer with Python and pygame installed.

import random
import pygame

pygame.init()
WIDTH, HEIGHT = 480, 360
GROUND = HEIGHT - 24  # where the ground starts
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Flappy Square")
clock = pygame.time.Clock()
font = pygame.font.Font(None, 30)
score_font = pygame.font.Font(None, 64)
big_font = pygame.font.Font(None, 56)

BG = (16, 18, 22)
GROUND_COLOR = (34, 38, 46)
YELLOW = (255, 212, 59)
BLUE = (48, 105, 152)
LIGHT_BLUE = (90, 162, 224)
RED = (235, 95, 85)
TEXT = (230, 232, 235)

GRAVITY = 0.35
FLAP = -6.2  # upward speed after a flap
GAP = 110  # space between the top and the bottom pipe
PIPE_WIDTH = 52
PIPE_SPEED = 3
PIPE_EVERY = 80  # frames between new pipes
FLAP_KEYS = (pygame.K_UP, pygame.K_SPACE, pygame.K_RETURN)

bird = pygame.Rect(100, 150, 24, 24)


def new_game():
    global bird_y, speed_y, pipes, pipe_timer, score, state
    bird_y = 150
    speed_y = 0
    bird.y = bird_y
    pipes = []
    pipe_timer = 0
    score = 0
    state = "ready"  # "ready", "playing" or "over"


def add_pipe():
    """A new pipe at the right edge, its gap not too far from the last one."""
    if pipes:
        gap_y = pipes[-1]["gap_y"] + random.randint(-90, 90)
    else:
        gap_y = HEIGHT // 2
    gap_y = max(70, min(gap_y, GROUND - 70))
    pipes.append({"x": WIDTH, "gap_y": gap_y, "scored": False})


def pipe_rects(pipe):
    """The top and bottom parts of a pipe."""
    top = pygame.Rect(pipe["x"], 0, PIPE_WIDTH, pipe["gap_y"] - GAP // 2)
    bottom_y = pipe["gap_y"] + GAP // 2
    bottom = pygame.Rect(pipe["x"], bottom_y, PIPE_WIDTH, GROUND - bottom_y)
    return top, bottom


def draw_text(text, text_font, color, center):
    image = text_font.render(text, True, color)
    screen.blit(image, image.get_rect(center=center))


new_game()
best = 0
wait = 0  # frames until a new game may start
running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.KEYDOWN and event.key in FLAP_KEYS:
            if state in ("ready", "playing"):
                state = "playing"
                speed_y = FLAP
            elif wait == 0:
                new_game()

    if wait > 0:
        wait -= 1

    if state == "playing":
        speed_y += GRAVITY
        bird_y += speed_y
        if bird_y < 0:  # the top of the screen is a ceiling
            bird_y = 0
            speed_y = 0
        bird_y = min(bird_y, GROUND - bird.height)  # and the ground is a floor
        bird.y = round(bird_y)

        pipe_timer -= 1
        if pipe_timer <= 0:
            add_pipe()
            pipe_timer = PIPE_EVERY
        for pipe in pipes:
            pipe["x"] -= PIPE_SPEED
            if not pipe["scored"] and pipe["x"] + PIPE_WIDTH < bird.left:
                pipe["scored"] = True
                score += 1
        # Forget pipes that have left the screen.
        pipes = [pipe for pipe in pipes if pipe["x"] > -PIPE_WIDTH]

        crashed = bird.bottom >= GROUND
        for pipe in pipes:
            top, bottom = pipe_rects(pipe)
            if bird.colliderect(top) or bird.colliderect(bottom):
                crashed = True
        if crashed:
            state = "over"
            best = max(best, score)
            wait = 40

    elif state == "over" and bird.bottom < GROUND:
        # After a crash the square drops to the ground.
        speed_y = max(speed_y, 0) + GRAVITY
        bird_y = min(bird_y + speed_y, GROUND - bird.height)
        bird.y = round(bird_y)

    screen.fill(BG)
    for pipe in pipes:
        top, bottom = pipe_rects(pipe)
        pygame.draw.rect(screen, BLUE, top)
        pygame.draw.rect(screen, BLUE, bottom)
        # A lighter band at each end of the gap.
        pygame.draw.rect(screen, LIGHT_BLUE, (top.x, top.bottom - 12, PIPE_WIDTH, 12))
        pygame.draw.rect(screen, LIGHT_BLUE, (bottom.x, bottom.y, PIPE_WIDTH, 12))
    pygame.draw.rect(screen, GROUND_COLOR, (0, GROUND, WIDTH, HEIGHT - GROUND))
    pygame.draw.line(screen, LIGHT_BLUE, (0, GROUND), (WIDTH, GROUND), 2)

    pygame.draw.rect(screen, RED if state == "over" else YELLOW, bird, border_radius=5)
    pygame.draw.rect(screen, BG, (bird.right - 9, bird.y + 6, 4, 4))  # an eye

    if state == "ready":
        draw_text("Flappy Square", big_font, YELLOW, (WIDTH // 2, 90))
        draw_text("Press Up or Space to flap", font, TEXT, (WIDTH // 2, 250))
    else:
        draw_text(str(score), score_font, TEXT, (WIDTH // 2, 40))
    if state == "over":
        panel = pygame.Rect(0, 0, 340, 140)
        panel.center = (WIDTH // 2, HEIGHT // 2)
        pygame.draw.rect(screen, BG, panel, border_radius=12)
        pygame.draw.rect(screen, BLUE, panel, 2, border_radius=12)
        draw_text("Game over", big_font, YELLOW, (panel.centerx, panel.y + 38))
        draw_text(f"Score {score}    Best {best}", font, TEXT, (panel.centerx, panel.y + 80))
        draw_text("Press Up or Space to try again", font, TEXT, (panel.centerx, panel.y + 112))

    pygame.display.flip()
    clock.tick(60)

pygame.quit()