Clumsy Bird

Flappy-style endless flyer with audio, score and restart.

▶ Run in browser

Upstream: https://github.com/ellisonleao/clumsy-bird

Licence: this port's own code is offered under GPL-3.0-or-later, not the SimVX Examples Licence the rest of the gallery carries. See ATTRIBUTION.md for the upstream it re-implements, the terms of anything it bundles, and the credit each one requires.

Ports live in the repository only, not in the simvx-examples distribution, because each is a derivative work licensed individually against the game it re-implements. Read it with git clone https://git.simvx.com/simvx/simvx.

Tags: port tier-0

Clumsy Bird (SimVX port)

SimVX port of ellisonleao/clumsy-bird, a MelonJS Flappy Bird clone: flap through pipe gaps, score per pipe passed, crash restarts.

Licensing: GPL-3.0-or-later (matching upstream code); all art is CC0 (Kenney Tappy Plane) or procedural, all audio synthesised. See ATTRIBUTION.md.

Run

simvx examples run ports_clumsy_bird            # interactive
simvx examples run ports_clumsy_bird --test     # headless capture
simvx export web main.py -o clumsy_bird.html

Controls

  • Tap / Space / controller A: flap (also starts a run, and restarts after a crash). The whole screen is the button, so a phone needs no on-screen pad.

  • Esc: quit

What it shows

  • One action, three devices: the root scene declares input_context = {"flap": [Key.SPACE, Mouse.LEFT, Controller.A], "quit": [Key.ESCAPE]}, so keyboard, mouse, touch and a controller share one code path. The web runtime delivers a tap as a left click, which is what makes the mouse binding the touch route. The context is in force while that scene is mounted; each scene declares its own, so a swap brings the new one’s in.

  • Event-driven input: the flap is handled by @on_input("flap"), which runs once per press, before any physics tick, and is handed the event that caused it.

  • A layout with no baked window size: the ground line, the bird’s rest height, the pipe gaps and the background are all computed from self.viewport_size in the hook that needs them, so the game plays at any window size and follows a resize. config.py names the size the window opens at and nothing else.

  • Readouts ignore the pointer: the score, the hint strip and the game-over panel set pointer_filter = PointerFilter.IGNORE, so a tap that lands on text still reaches the game.

  • Coroutines: pipe spawning is a generator driven by start_coroutine and wait, not a hand-rolled timer field.

  • Signals: the bird emits flapped and crashed, each pipe pair emits passed; the play scene connects them and keeps the score.

  • AnimatedSprite2D: a three-frame flipbook drives the wing beat.

  • Procedural audio: the wing, score, crash and looping theme are synthesised at load with AudioSynth, so the port ships no sound files.

  • Anchored UI: the score readout, title card, bottom controls strip and game-over panel are Controls positioned with anchors and margins.

  • Scene flow: the title screen and every restart are tree.change_scene swaps.

Source files

File

Summary

Lines

main.py

Clumsy Bird: Flappy-style endless flyer with audio, score and restart.

59

config.py

Shared constants and asset paths for Clumsy Bird.

72

nodes/__init__.py

Clumsy Bird scene nodes.

1

nodes/audio.py

Procedural audio: built on the engine’s AudioSynth API.

129

nodes/bird.py

Player bird: gravity, flap, rotation.

158

nodes/ground.py

Scrolling ground: two side-by-side ground sprites that loop, plus a

65

nodes/hud.py

Score HUD: anchored Label that displays the current score.

27

nodes/menu.py

MenuScene: the title screen shown before a run starts.

116

nodes/pipe.py

Pipe pair: top + bottom pipe sprites that scroll right to left and emit

88

nodes/play_scene.py

PlayScene: the active game: bird, pipes, ground, HUD, score, restart.

286

Source

 1#!/usr/bin/env python3
 2# /// simvx
 3# tags = ["port", "tier-0"]
 4# upstream = "https://github.com/ellisonleao/clumsy-bird"
 5# web = { width = 900, height = 600, responsive = true }
 6# ///
 7"""Clumsy Bird: Flappy-style endless flyer with audio, score and restart.
 8
 9Run desktop:   simvx examples run ports_clumsy_bird
10Headless:      simvx examples run ports_clumsy_bird --test
11Web export:    simvx export web main.py -o clumsy_bird.html
12
13Controls:
14    Tap / Space / (A)    Flap (also starts a run and restarts after a crash)
15    Esc                  Quit
16"""
17
18# /// script
19# requires-python = ">=3.14"
20# dependencies = ["simvx-core", "simvx-graphics", "numpy", "pillow"]
21# ///
22
23from __future__ import annotations
24
25import sys
26from pathlib import Path
27
28_PORT_DIR = Path(__file__).resolve().parent
29
30from config import WINDOW_HEIGHT, WINDOW_WIDTH  # noqa: E402
31from nodes.menu import MenuScene  # noqa: E402
32from nodes.play_scene import PlayScene  # noqa: E402
33
34from simvx.graphics import App  # noqa: E402
35
36
37def main():
38    test_mode = "--test" in sys.argv
39    app = App(width=WINDOW_WIDTH, height=WINDOW_HEIGHT, title="Clumsy Bird", visible=not test_mode)
40
41    if test_mode:
42        from simvx.graphics import save_png
43
44        out = _PORT_DIR / "screenshots"
45        out.mkdir(exist_ok=True)
46        # A headless run gets no input, so boot straight into the play scene:
47        # the captures then show the game rather than the title card.
48        frames = app.run_headless(PlayScene(), frames=180, capture_frames=[60, 120, 179])
49        for idx, n in enumerate([60, 120, 179]):
50            save_png(frames[idx], out / f"frame_{n}.png")
51        print(f"Wrote {len(frames)} screenshots to {out}")
52        app.quit()
53        return
54
55    app.run(MenuScene())
56
57
58if __name__ == "__main__":
59    main()