Mini Space Shooter: a pixel-art Linux space shooter whose enemies learn how you fly
Mini Space Shooter is a small pixel-art space shooter for Linux, written in C11 and C++17. It has endless levels, and its enemies use online learners to adapt their movement and tactics to how you fly. Raw difficulty is capped by hard limits, so later levels add tactical variety rather than unlimited speed or bullet counts.
- Platform
- Linux (Wayland and X11)
- Language
- C / C++
- Licence
- MIT
- Requires
- build-essential, libx11-dev, libxext-dev
- Optional: SDL2 runtime for native Wayland
- Run
./run.shfrom the repository folder
How do I run it?
./run.sh
Building needs build-essential, libx11-dev and libxext-dev. On a Wayland desktop the game uses the optional SDL2 runtime (libsdl2-2.0-0) for a native window and needs no SDL headers; on X11, or when native Wayland is unavailable, it uses its Xlib renderer. Audio loads ALSA or PulseAudio dynamically and can run silently.
If an icon appears but no window opens, choose the backend explicitly: MSS_VIDEO_BACKEND=wayland ./run.sh or MSS_VIDEO_BACKEND=x11 ./run.sh. The README documents a GNOME/Wayland case where the X11 window stayed unmapped, which the native Wayland path bypasses.
What limits keep the difficulty fair?
| Limit | Value |
|---|---|
| Enemy bullet speed, including horizontal movement | 74 px/s |
| Enemy horizontal / vertical speed | 36 / 46 px/s |
| Minimum time between volleys from one enemy | 0.55 s |
| Enemies / enemy bullets on screen | 12 / 24 |
| Minimum spawn interval | 0.85 s |
| Minimum new-shot reaction time | 0.30 s |
| Post-hit ceasefire | 1.10 s |
| Scheduled inbound corridors per arrival window | At most 4 of 5 |
The README is explicit that the corridor rule concerns straight bullet trajectories at the bottom row. It is not a proof that a player can reach the corridor from every position, and human playtesting is still needed for balance and feel.
How do the enemies learn?
All gameplay learners are allocation-free and written in the project's own src/cpp/ml.*:
- A two-layer MLP chooses advance, strafe left or right, or retreat, trained by policy gradient (the full softmax entropy derivative, checked against finite differences).
- An intent model learns from features 15 simulation steps earlier and predicts from current features.
- A stress model uses a 120-step delayed window of recent damage.
- An eight-arm bandit ages its evidence, so it can change preference as the player changes.
- Online k-means with bounded history groups play styles; tactics follow the cluster's measured style.
Learning survives restarts within the same process but is not saved between launches.
What are the controls?
| Key | Action |
|---|---|
| Arrows or WASD | Move |
| Space, Z or J | Fire |
| Esc or P | Pause |
| 1, 2, 3, 4 | Buy Scout, Wing, Cruiser, Titan |
| M, N | Mute all audio, toggle music |
| F1 | Learning and difficulty overlay |
How is it tested, and where is progress saved?
./run.sh test runs headless regression tests (35 according to the README), and strict, sanitize, shots, audio-test and window-test cover warnings-as-errors builds, AddressSanitizer and UBSan, screenshots of every screen, audio, and a real-window input test. The save is ~/.local/share/mini-space-shooter/save.txt (or under $XDG_DATA_HOME); writes use a temporary file and an atomic rename, and --reset-save explicitly replaces it with defaults.
Quick answers
- Does the game keep getting harder forever?
- No. Raw difficulty has hard limits, such as an enemy bullet speed of 74 px/s and a minimum of 0.55 s between volleys from one enemy. Later levels add tactical variation instead.
- Does the enemy learning carry over between sessions?
- Not across launches. Learning survives restarts in the same process but is not persisted to disk.
- Where is my progress saved?
- In ~/.local/share/mini-space-shooter/save.txt, or under $XDG_DATA_HOME/mini-space-shooter/save.txt.
- What if no window opens on Wayland?
- Select the backend explicitly with MSS_VIDEO_BACKEND=wayland ./run.sh or MSS_VIDEO_BACKEND=x11 ./run.sh. The default launcher already picks native Wayland when WAYLAND_DISPLAY is set.