A 2D arcade shooter in Ruby with an optional AI wingman. Originally built from the Learn Game Programming with Ruby tutorial in 2018; modernized, refactored, and extended with classical game AI in 2026.
I built this game in 2018 while working through a game programming book. It worked, and I moved on. Coming back to it in 2026, I found the kind of code you'd expect from someone learning to program: everything in one class, no tests, a typo in a constant (WIDOW_HEIGHT), and a movement function that had never worked correctly.
The goal of this revision wasn't to rewrite it — it was to do what a senior engineer actually does: understand the existing system, add tests to pin the behavior, refactor to a cleaner architecture, then extend it with something new. The new thing is an AI wingman.
Top-down arcade shooter. You control a ship at the bottom of the screen. Enemy ships descend from the top. Destroy them before they reach you or you fly too close to the MotherShip.
Controls:
- Arrow keys — turn and thrust
- Space — fire bullet
- C (hold) — fire laser (unlocked at 50 kills)
- V (hold) — fire blaster (unlocked at 150 kills)
Start screen:
- W — toggle AI wingman on/off
- Space — start
The optional AI wingman is built on classical game AI — not an LLM. The reason: arcade games run at 60 frames per second. A wingman that makes an API call to decide where to fly would be both too slow and wildly overkill for a spatial reasoning problem that decades-old algorithms solve perfectly.
The wingman runs a four-state finite state machine:
ESCORTING ──→ ENGAGING enemy enters engagement range (200px)
ENGAGING ──→ ESCORTING no enemies in range
ANY STATE ──→ EVADING enemy within collision range (60px) — takes priority
EVADING ──→ RETURNING collision threat clears
RETURNING ──→ ESCORTING wingman back in formation slot
Each state delegates to a specialized component:
FormationKeeper— computes the wingman's formation position relative to the player, clamped to screen boundsTargetPrioritizer— scores enemies by proximity and trajectory (dot product of velocity toward player)PredictiveAimer— solves the quadratic intercept equation to compute lead angle for a moving targetWingmanStateMachine— drives state transitions based on observed game state
The wingman fires at a low probability per tick (not every frame) and occasionally misses. Perfect AI isn't more impressive — it's less fun.
Requires Ruby 3.2+ and the Gosu native library dependencies for your platform. See the Gosu wiki for system-level setup.
bundle install
ruby sector_five.rblib/
entities/ # Ship, Player, Wingman, Enemy, Projectile, Bullet, Laser, Blaster, Explosion
systems/ # CollisionDetector, WeaponProgression
scenes/ # StartScene, GameScene, EndScene
wingman/ # FormationKeeper, TargetPrioritizer, PredictiveAimer, WingmanStateMachine
spec/ # mirrors lib/ — pure-logic components only
sector_five.rb # entry point
GameWindow manages scene transitions and delegates update/draw/button_down to the active scene. GameScene owns all game state and coordinates entities, systems, and the wingman each tick.
The wingman AI components are pure Ruby modules with no Gosu dependency — fully unit-testable and deliberately separated from the Wingman entity that wires them together.
bundle exec rspec~65 examples covering collision math, weapon progression thresholds, projectile motion, ship physics, and all four AI components. Game code that requires Gosu (rendering, input, window) is excluded from tests by design.
bundle exec rspec spec/wingman/ # AI components onlyv1.x possibilities:
- Smarter enemies that dodge bullets (reuses
PredictiveAimerlogic) --difficultyflag to tune wingman miss rate and reaction time- Replay system
Cosmetic stretch (if LLMs earn a place):
- Between-wave wingman banter — language work at acceptable latency, not gameplay decisions
Music by Kevin MacLeod (incompetech.com), licensed under Creative Commons Attribution. See credits.txt for full attribution.
Original tutorial: Learn Game Programming with Ruby by Mark Sobkowicz.
MIT — see LICENSE.