Overview
Z SHELL is a cyberpunk story game that teaches Unix from zero. You play rook, woken at 03:14 by a machine you did not turn on, and every move you make is a real shell command typed at a real prompt. Fifteen missions across five arcs, 45 objectives, 47 commands, each with its own man page. By the end you are reading process tables and chaining grep into awk because the story needed you to, not because a tutorial told you to.
It began as a pure-Python terminal game and is now a native iOS app in TestFlight beta. The build-and-ship story is written up separately in Shipping to TestFlight Without Opening Xcode.
Nothing You Type Touches the Real Machine
The hard invariant of the whole project: Z SHELL never executes a host command and never touches the real filesystem, network, or processes. curl, ssh, ps, kill, and the other 43 are simulations running against a virtual filesystem the engine owns entirely.
That is a teaching decision before it is a safety one. A learner has to be able to rm the wrong thing, kill the wrong process, and chmod a file into uselessness, then see exactly what that costs, with no possibility of it costing anything. A sandbox that only mostly holds cannot make that offer.
Instruments Are Earned, Never Given
Most teaching tools hand you the whole interface on the first screen and hope you grow into it. Z SHELL starts you with a bare prompt and nothing else, and every capability arrives as a story beat you had to reach.
A HUD, then lens, scope, pulse, deck. Each is installed by running kopeng install <name> at the moment the plot requires it, so the interface grows as the player does and never shows a control that has no meaning yet.

Two other mechanics do the retention work:
- Failure-first objectives. Several objectives require you to observe the instructive error before the fix is unlocked. You cannot be handed the right incantation until you have seen the wrong one fail and understood why.
- Spaced repetition. A Leitner-scheduled recall system resurfaces commands you have learned at widening intervals, so mission 12 quietly quizzes you on something from mission 3.
The Port Is Graded Against the Original
The iOS version is a SwiftUI app on top of EngineKit, a pure-Foundation Swift package holding the shell, the virtual filesystem, the campaign, grading, warmups, and save/load. No UI dependency anywhere in it, so the entire engine runs and tests headless.
The port is not validated by tests I wrote about what Swift should do. It is validated against the Python that already worked. A differential harness generates committed goldens from the original game and replays the identical inputs through EngineKit, and any divergence between the two is by definition a bug in the Swift.
It found real ones. Python's shlex raises on a trailing backslash rather than passing it through; posixpath preserves exactly two leading slashes. Both are correct POSIX behaviour, both are invisible unless you go looking, and a hand-written Swift test would have rubber-stamped whatever the port happened to do.
On top of that: 163 EngineKit unit tests and 12 XCUITest UI tests, green from a clean clone, with the full 15-mission campaign completing under two different random seeds driven purely through the public submit() API. Then a read-only AI review at maximum reasoning over the whole port diff, which surfaced 11 further defects (save and restore data loss, an out-of-range index crash, instrument fidelity drift). Every one was verified against the source and fixed.
The Build Never Opened a GUI
Development ran on Windows 11 and built on a Mac mini reached only over SSH. The Xcode GUI was never opened, not once. The project is XcodeGen YAML rather than a checked-in .pbxproj so it stays editable from a headless box; the app icon is drawn by a Python script; UI tests run on the iPhone 15 simulator named explicitly, because the default current-generation device is 9 points wider than the 393pt design target and would have false-passed the rule that the HUD must never truncate.
App Store Connect was stood up from scratch through its REST API: bundle ID registration, cloud-signed archive, signed IPA export, TestFlight upload, export compliance, internal testing group, tester. Build v1 processed to VALID and installed on a phone. The only step the API would not do was create the app record itself.
Results
- 15 missions, 45 objectives, 47 simulated commands, each with a man page
- 175 tests green from a clean clone: 163 EngineKit unit, 12 XCUITest UI
- Two full campaign runs under different seeds, headless, through the public API
- 11 AI-review defects found, verified, and fixed before the first signed build
- Zero host commands executed, ever, by design
A game about a shell, written twice, the second version graded against the first, and shipped by a machine that never showed anyone a window.