A puzzle game of bent arrows sent off a board of dots, written in Zig.
  • Zig 94.1%
  • Nix 5.7%
  • Shell 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Jeffrey C. Ollie 8efdb71b60
All checks were successful
test / test (push) Successful in 19m29s
Play on Android, as a NativeActivity with no Java
The Android shell is a shared library loaded by NativeActivity. Its
callbacks hand messages to the game's own looper thread and wait for
them to be acted on. Frames are paced by the choreographer and drawn by
the same Vulkan renderer as Linux, into a swapchain on the window.
JNI asks Java for the system accent, the insets, and to hide the bars.

zig build apk links the NDK's system libraries for arm64-v8a and x86_64,
puts them in a package that aapt2 made, using tools/apk.zig, which
stores them aligned to 16 KiB, and signs it with apksigner.
install-apk and run-android install it with adb; nix run .#emulator
starts an emulator.

Along the way:
- the renderer takes its format, and gains a render pass that leaves
  a swapchain image ready to present;
- the board can be fitted inside a safe area;
- the level store moves out of the Wayland shell;
- OpSource's language is made Unknown when the shaders are embedded,
  since SwiftShader rejects Zig's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QKsfJ57x3ZAv83X1SccVTQ
2026-10-24 01:53:05 -05:00
.forgejo/workflows Play on Android, as a NativeActivity with no Java 2026-10-24 01:53:05 -05:00
android Play on Android, as a NativeActivity with no Java 2026-10-24 01:53:05 -05:00
dbus The settings file and the desktop's accent color 2026-10-22 23:27:38 -05:00
dist Package the game with Nix, with a desktop entry and an icon 2026-10-23 00:33:59 -05:00
docs/adr Play on Android, as a NativeActivity with no Java 2026-10-24 01:53:05 -05:00
LICENSES The rules of the game, generator, solver, and repository setup 2026-10-22 23:05:55 -05:00
nix Play on Android, as a NativeActivity with no Java 2026-10-24 01:53:05 -05:00
shaders Play on Android, as a NativeActivity with no Java 2026-10-24 01:53:05 -05:00
src Play on Android, as a NativeActivity with no Java 2026-10-24 01:53:05 -05:00
tools Play on Android, as a NativeActivity with no Java 2026-10-24 01:53:05 -05:00
.gitignore The rules of the game, generator, solver, and repository setup 2026-10-22 23:05:55 -05:00
build.zig Play on Android, as a NativeActivity with no Java 2026-10-24 01:53:05 -05:00
build.zig.zon Package the game with Nix, with a desktop entry and an icon 2026-10-23 00:33:59 -05:00
build.zig.zon.nix Package the game with Nix, with a desktop entry and an icon 2026-10-23 00:33:59 -05:00
CONTRIBUTING.md Play on Android, as a NativeActivity with no Java 2026-10-24 01:53:05 -05:00
flake.lock Package the game with Nix, with a desktop entry and an icon 2026-10-23 00:33:59 -05:00
flake.nix Play on Android, as a NativeActivity with no Java 2026-10-24 01:53:05 -05:00
GLOSSARY.md The rules of the game, generator, solver, and repository setup 2026-10-22 23:05:55 -05:00
package.nix Play on Android, as a NativeActivity with no Java 2026-10-24 01:53:05 -05:00
README.md Play on Android, as a NativeActivity with no Java 2026-10-24 01:53:05 -05:00
REUSE.toml Package the game with Nix, with a desktop entry and an icon 2026-10-23 00:33:59 -05:00

pfeilstorch

A puzzle game in Zig. A board of dots is covered by bent arrows, and tapping an arrow sends it off the board in the direction it points — if the straight line from its head to the edge is clear. If another arrow is in the way, the arrow stays put and a life is lost; three lives are allowed per level. Clear the board to win the level. It is in the style of Arrow Puzzle Game.

The name is German for "arrow stork": the white stork that turned up in Mecklenburg in 1822 with an African spear through its neck, and with it the answer to where storks go in winter.

Where it stands

It plays on Linux, in a Wayland window drawn with Vulkan, and on Android. On Linux:

$ nix develop
$ zig build run

Click or touch an arrow to send it off, and drag anywhere off the board to move the window. r starts the level over, and q or Escape quits. The level reached is kept in $XDG_STATE_HOME/pfeilstorch (~/.local/state/pfeilstorch), and --level N plays another. An iPhone and iPad version may follow.

An arrow is black until it is tapped. One that leaves turns the accent color on its way out, which is the desktop's own (from the XDG desktop portal, followed as it changes) unless the settings file names one; one that is blocked turns red, and stays red until it leaves. The settings file is $XDG_CONFIG_HOME/pfeilstorch/config.struthio (~/.config/pfeilstorch/config.struthio), in struthio, and everything in it may be left out:

.{
    // An arrow on its way out; the desktop's accent color if left out.
    .accent_color = "#3584e4",
    // A blocked arrow, and a life lost.
    .block_color = "#e01b24",
}

The code is in three layers, of which only the last knows what it runs on:

  • src/rules: the game, with no platform in it — no I/O, no clock:
    • the board, its arrows, and what a tap does: an exit, or a block naming the arrow in the way and how far off it is;

    • lives, and winning and losing a level;

    • a level generator that builds solvable boards from a seed. A level's seed comes from its number alone, so level 37 is the same board for everyone. Arrows are placed one at a time, each with a clear line to the edge past the ones placed before it, so removing them in reverse always works;

    • a solver. Since an arrow leaving only ever frees dots, the order of exits never matters, and taking any arrow that can leave until none can decides whether a board can be cleared;

    • boards as text, for levels written by hand and for tests:

      # Arrow 0 points right at arrow 1, which points up and out.
      4x2
      0,1 1,1
      3,1 3,0
      
  • src/view: the game on a screen, still with no platform in it: where the board goes in a window, which arrow a tap is on, exits sliding off with the body following the head like a snake, blocks bumping against the arrow in the way, and each frame as a list of circles, lines and triangles.
  • The Android shell, in src/android, is a library that Android's own NativeActivity loads, so the app has no Java in it at all; what only Java can say, the accent color and where the camera cutout is, is asked through JNI. It draws with the same Vulkan code as Linux, into a swapchain on the window.
  • The Linux shell. The window and its input are Wayland, through zig-wayland-native and zig-xkb, with no libwayland or libxkbcommon. The shapes are drawn by Vulkan, through vulkan-zig, with shaders written in Zig and compiled to SPIR-V, each shape covered by its signed distance so that edges are smooth at any size. The desktop's accent color comes over D-Bus through mdbus and zig-dbus-service. Frames reach the compositor as dma-bufs it reads directly, or through shared memory where it and the GPU cannot share them; that part follows harrier, which does the same for a terminal.

--snapshot FILE draws one frame into a PNG with no window, for looking at a change to how the game draws without a screen:

$ zig build run -- --snapshot level30.png --level 30
$ zig build run -- --snapshot exit.png --tap 3,0 --at 80

The words used for the parts of the game are defined in GLOSSARY.md, and the decisions that shape the code are in docs/adr.

Getting it

$ git clone https://git.jcollie.dev/jeff/pfeilstorch.git

The same repository is mirrored on Tangled at https://tangled.org/jcollie.dev/pfeilstorch.

Installing

The flake has the game as a package, with a desktop entry and an icon so that GNOME Shell and other desktops list it:

$ nix run git+https://git.jcollie.dev/jeff/pfeilstorch.git
$ nix profile install git+https://git.jcollie.dev/jeff/pfeilstorch.git

Android

The game needs Android 10 or later and a device with Vulkan 1.2, which is most made since 2020. It fills the screen, in either orientation, with the system bars hidden until a swipe from the edge brings them back. Tap an arrow to send it off; back sends the game to the background, as it does any app. An arrow on its way out takes the accent color Android 12 and later draw from the wallpaper. The level reached is kept in the app's own storage.

There is no download yet. To build the package and install it on a phone with USB debugging turned on:

$ nix develop
$ zig build apk -Doptimize=ReleaseSafe
$ zig build run-android -Doptimize=ReleaseSafe

zig build apk leaves the package in zig-out/pfeilstorch.apk, and run-android installs it with adb, starts it, and follows its log. With no phone, nix run .#emulator starts an Android emulator for adb to find.

Building

Everything comes from the Nix flake:

$ nix develop
$ zig build test --summary all
$ zig build test --fuzz=1M
$ zig build run

It needs Zig 0.17.0, and to play, a Wayland compositor and a Vulkan driver. The Android build also needs the Android SDK and NDK, which the flake provides. Linking a C library is a decision taken one library at a time, and so far there are none beyond what the operating system is reached through: the C library the Vulkan loader needs on Linux, and Android's own system libraries; see ADR 0001.

Issues and pull requests

Report bugs and send pull requests on the Tangled mirror: https://tangled.org/jcollie.dev/pfeilstorch. See CONTRIBUTING.md.

License

MIT. The project follows the REUSE specification.

References cited