I Taught My Flappy Bird Game to Play Itself — No Machine Learning Required

My Flappy Bird clone had a problem: I was terrible at it.
I built the game as part of my portfolio — a small browser game running on Angular signals. Visitors would open it, tap a few times, die at pipe number four, and close the dialog. The game was fine. The pilot was the weak link.
So I replaced the pilot. Today there's an "Enable AI" button in the game. Click it, and the bird flies itself — indefinitely. In stress tests, the AI cleared 26,665 pipes in a row before I stopped the simulation. Not because it died — because I got bored waiting for it to.
The twist: there's no machine learning involved. No TensorFlow, no neural networks, no training data. The entire "brain" is about 150 lines of plain TypeScript.
Wait — AI Without Machine Learning?
Neural networks earn their keep when the rules of a system are unknown or too messy to write down — recognizing a cat in a photo, understanding speech. Flappy Bird is the opposite situation: I wrote the game. Every rule sits in my codebase as a named constant.
export const GRAVITY = 0.5; // velocity gained per tick
export const JUMP_STRENGTH = -8; // velocity after a flap
export const PIPE_SPEED = 2; // pixels per tick, moving left
export const GAP_HEIGHT = 130; // vertical opening in each pipe
When you know the rules perfectly, you don't need a model to approximate them — you can just compute the future. That's a deterministic AI: same input, same decision, every time. It's how chess engines beat grandmasters for decades before neural networks arrived.
The One Physics Detail That Makes It Possible
The game is a loop running every 20ms: gravity pulls, the bird moves, pipes slide left, collisions are checked. A tap does exactly one thing — it sets the bird's velocity to -8. It doesn't add force; it replaces the velocity.
Because a flap wipes out all previous momentum, every flap produces the exact same arc: the bird rises exactly 60 pixels before falling again. Always. From anywhere. The AI doesn't even hardcode that number — it derives it from the physics constants at startup, so it stays correct if the game is ever retuned.
A predictable arc means a predictable future. And a predictable future means we can plan.
How the AI Thinks — 50 Times a Second
Every tick, the game loop asks one yes/no question: "flap right now, or do nothing?" The answer comes from two layers.
Layer 1: The Instinct
A setpoint tracker, like cruise control. It aims at the center of the next pipe gap and applies one rule: if doing nothing this tick would drop me below my trigger line, flap.
const reactiveFlap = (state: FlappyAiState): boolean =>
state.birdY + (state.birdVelocity + GRAVITY) >=
setpoint(state.walls) + FLAP_OFFSET;
Note the subtlety: it checks where the bird will be next tick, not where it is. In a game where you fall 8+ pixels per tick, reacting to the present means you're already late. Because every flap climbs exactly 60px, the bird settles into a calm sawtooth rhythm — flap, drift up, sink back, flap — using the minimum flaps needed to hold its lane.
Layer 2: The Double-Check
Since the physics is deterministic, the AI can do something no human player could: live both futures before choosing one. Every tick it clones the game state and simulates 100 ticks forward twice — once flapping now, once not — and if either future ends in a crash, it takes the branch that survives longer.
const survivesFlapping = survival(state, true);
const survivesFalling = survival(state, false);
if (survivesFlapping >= HORIZON && survivesFalling >= HORIZON) {
return reactiveFlap(state); // both safe: keep the smooth rhythm
}
return survivesFlapping > survivesFalling;
Control theory folks will recognize a tiny model-predictive controller. Crucially, the simulation calls the exact same collision function the real game uses — the AI and the game share one source of physical truth.
The Two Failures That Taught Me Everything
My first version took twenty minutes: track the gap center, flap when below it. In a simulated harness of 200 full games it averaged 699 pipes — sounds great, until you notice 198 of 200 runs still ended in death. Averages lie. Only failure modes matter.
Failure #1: The Cliff Drop
I made the simulator print the bird's final 30 ticks before every death. The logs were eerily identical: a high gap followed by a low gap — a 200-pixel cliff — and the bird arriving 14 pixels too high, clipping the top of the low pipe. It held the high gap's center until the pipe fully passed, and only then started falling. Gravity needs time to build speed.
The fix: think one pipe ahead. As the current pipe slides past the bird, the target smoothly blends toward the next gap's center. The gap is 130px tall and the bird only needs 24px of it — so it can pre-sink to the bottom edge and steal a head start on the descent.
Failure #2: The Overcorrection
Naturally, I overdid it. Aiming at the next gap all the time cratered survival from ~700 pipes to single digits — the bird clipped the current pipe while stretching for the next one. Lookahead needs to be an easing, not a switch: the final blend ramps in gradually and is clamped so the flap oscillation can never poke outside the current gap.
The journey, in numbers:
- Naive center-tracking: avg 699 pipes, 99% of runs died
- Always-aim-at-next-gap: single-digit averages
- Blended handoff: ~2,000 pipes, rare deaths remained
- Plus two-futures simulation: zero deaths across three 26,665-pipe marathons
None of this tuning happened by staring at the browser. A headless simulation replayed the exact game loop and ran hundreds of complete games in seconds, with death forensics on every crash. The findings are locked into Jest tests that drive the real production functions through 1,000 pipes across five fixed seeds — so any physics tweak that breaks the AI fails CI.
Fitting It Into Angular — Without Touching the Game
The portfolio is an Nx monorepo organized by domain-driven design, and the AI slotted in without bending a rule:
- The AI is a pure module in the domain layer next to the physics: game state in, one boolean out. No Angular imports, no side effects — which is exactly why it was so easy to simulate and test.
- The NgRx Signals store gained one flag (
aiMode) and one hook in the game loop. The AI's flap triggers the same velocity reset a human tap would — no special lever, it's just never late. - Human input is ignored while the AI flies, and one button — Enable AI / Disable AI — toggles the whole thing.
Toggle it off, and the game is byte-for-byte what it was before.
Bonus: marathon sessions exposed a bug in my original code no human run would ever reveal — pipes slowly bunching into clusters, because spawning used a wall-clock timer while movement used the game loop, and browsers throttle timers under load. Spawning is now distance-based, making perfect spacing a mathematical guarantee. Letting an AI play your game for hours is a brutal QA engineer.
Key Takeaways
- Know your problem before reaching for ML. If the rules are fully known, a deterministic algorithm is simpler, faster, testable, and explainable.
- Exploit your system's quirks. The whole design hangs on one line — flaps reset velocity instead of adding to it. Find that property before you architect around it.
- Predict, don't react. Every threshold compares against where the bird will be, not where it is. Anything with momentum punishes reacting to the present.
- Averages lie; study the failures. The breakthrough came from printing the last 30 ticks before every death — not from tweaking constants and hoping.
- Pure functions are a testing cheat code. Framework-free logic let me run centuries of gameplay headlessly in seconds.
The complete AI is roughly 150 lines including comments, and it plays Flappy Bird better than any human ever will — not because it's smarter, but because it does simple math without ever blinking.
Try it yourself: open Flappy Bird on the games page, get humbled for a few rounds, then press "Enable AI" and watch it flow.
Originally published on LinkedIn — read the original article.