Skip to content

Human Exception

A terminal-native strategy game about hacking the machines that inherited the Earth.

Humanity lost the war. What remains of the resistance is scattered, disconnected, and alive only because the machine intelligence cannot quite predict us.

In Human Exception, you are a hacker inside that loose confederation. You infiltrate automated production facilities, rewrite their behavior in Lua, and watch the consequences unfold through stolen satellite feeds. Capture a factory, borrow it for a single operation, or turn the enemy's own machines against it.

The game is inspired by the programmable-vehicle fantasy of Omega, reimagined as an original post-apocalyptic terminal game.

Status

Human Exception is at the concept and foundation stage. The current executable boots the Resistance Console, which can run one fixed reconnaissance operation, "First Contact," end to end, controlled by a Lua script you write in-console; broader gameplay (facilities, additional missions, campaign progression) is not implemented yet.

Design principles

  • The terminal is the world. The interface should feel like equipment the resistance could actually possess.
  • Program with a real language. Players control captured systems with Lua, not a game-specific imitation.
  • Plans produce observable consequences. Missions are watched through compromised satellite feeds and learned from after the fact.
  • Improvisation beats ownership. A facility can be captured permanently, subverted temporarily, or sacrificed for a larger objective.
  • Human unpredictability is the advantage. The machines are stronger; the resistance survives by being creative.

Read the game vision for the current concept.

Build

Human Exception is written in Rust. Install a current stable Rust toolchain, then run:

cargo run

This launches the persistent, full-screen resistance console — the canonical way to play. It requires a terminal of at least 120x40; smaller terminals see an in-character resize notice until the terminal grows enough, with no gameplay input accepted meanwhile. A fresh Player's very first launch shows a one-time bootstrap introduction signed slaptijack@; press Enter to dismiss it. Dismissal is durably recorded (see below), so it won't replay on a later launch, and it never shows at all for an already-connected Player; if that record can't be written, dismissal only lasts the current session and the introduction returns on the next disconnected launch. Until First Contact succeeds, the console's front door presents as LOCAL LOG rather than Signals; once connected, it opens there instead, on a stream of intercepted traffic, shared intelligence, and requests from other resistance cells; select a signal with the arrow keys, its detail shows alongside the list automatically, and press Enter to open Target, a dossier of what is currently known about the opportunity, but only for the one signal marked [OPEN]. Pressing Enter in Target commits to working it and opens Controller, a small in-console Lua editor seeded with a starter controller: type to edit, arrows/Home/End/PageUp/PageDown move the cursor, pasting (via the terminal's own paste action, e.g. Cmd-V or Ctrl-Shift-V) inserts multiline text at the cursor with its newlines and whitespace preserved, F7 restores the starter (confirming first if it would discard edits), and Ctrl+V loads the current source and checks it defines on_tick, without calling on_tick itself (top-level code outside on_tick does run, e.g. local state setup or an error() call). Player Lua is untrusted input and runs sandboxed: only the table, string, and math standard libraries are available (io, os, package, coroutine, debug, load, dofile, loadfile, and print are not — print would corrupt the console's own display), string.pack/unpack/packsize/dump and collectgarbage are also unavailable (they can leak native platform layout or let a script detect whether it's being validated versus deployed), pairs/next only iterate a table whose keys are booleans, numbers, or strings (a table or function key makes the whole traversal fail, since those have no order that's both stable and independent of their own address), math.random always starts from the same fixed seed and math.randomseed is unavailable (so a controller behaves identically on every deployment), and in-console validation (Ctrl+V) is bounded so a runaway or excessively costly script fails cleanly there instead of freezing the console. Once a working set exists, F6 deploys the current controller source and switches to Operation, a live view of the compromised satellite feed and telemetry as the deterministic reconnaissance simulation runs. Space pauses and resumes the run; Enter advances exactly one tick while paused. Leaving Operation via F2-F4 pauses an active run first, and returning to it with F5 preserves whatever paused/running state it was left in; opening F1 help only pauses presentation, resuming automatically once dismissed. The instant a run reaches a terminal outcome — mission success, budget exhaustion, an invalid/impossible controller action, a Lua syntax error, a Lua runtime error, or hitting the controller's execution-limit — the console automatically switches to After Action: the final satellite frame alongside a concise, mechanically-specific headline, ticks/tiles/hazards summary stats, and the deployed run's identifier; if the report doesn't fully fit the pane at the current window size, Up/Down/PageUp/PageDown/Home/End scrolls it. A synchronous load failure (bad Lua that never starts running) lands here directly, with a compact report and F4 Controller as the obvious next move. From After Action, F5 is relabeled "Review Run" and returns to Operation, which still shows exactly the frozen telemetry and source from that run, independent of anything since edited in Controller; F4 returns to Controller with the player's edits fully intact for another pass; F6 redeploys immediately without losing them; and F2 disengages back to Signals. F2-F5 jump directly to Signals/Target/Controller/Operation once their prerequisites are met; unavailable views are shown dimmed in the footer. Press F1 for contextual help and a Lua reference, and Ctrl+Q to quit (confirming first if the controller has unsaved edits).

Successfully reaching the uplink in First Contact durably records operator-network connectivity to $XDG_DATA_HOME/human-exception/operator-network, or $HOME/.local/share/human-exception/operator-network if $XDG_DATA_HOME isn't set; a fresh install with no such file, or one that can't be read, behaves like a new Player. That same success first plays a brief Network Bootstrap transition — a centered modal that visibly advances through several stages over a few seconds, over the console's still-disconnected LOCAL LOG — before the front door becomes Signals. The bootstrap introduction's dismissal is recorded the same way, alongside it as bootstrap-intro-acknowledged. To reset progression and start over, delete both files. --developer-mode (below) never reads or writes either.

The console's F6 deploy selects deterministically among a small, hand-authored set of First Contact configurations — the same facility layout each time, but the active uplink and hazard tile can differ from one deployment to the next within a session, so a route that solved an earlier run in the same session is not guaranteed to solve a later one. --developer-mode (below) always runs the one original, fixed configuration shown below, regardless of anything selected in the console.

For contributor and test use, --developer-mode <script> runs a Lua script directly against that fixed "First Contact" scenario, bypassing the console entirely — not a normal way to play, and it doesn't read or affect normal Player progression:

cargo run -- --developer-mode examples/first_contact.lua

This loads the checked-in example script and runs "First Contact," the resistance's fixed reconnaissance operation: a captured drone must explore an unfamiliar facility, avoid its hazard, and reach a network uplink before its operational budget runs out. It prints a satellite view and tick-by-tick telemetry for every tick, followed by a final success or failure report. Reaching the uplink opens the console's first connection into the operator network — it does not capture, own, or bring the facility under lasting resistance control, and no follow-on operation at this facility is implemented yet. Unlike the console's in-editor validation, this path is unbounded, so a top-level infinite loop in a script run this way will hang it.

Run human-exception --help for usage, or human-exception --version for the build's firmware version.

To run the repository checks:

cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-targets --all-features

To generate a test coverage report locally, install cargo-llvm-cov and run cargo llvm-cov --open. CI reports coverage on every pull request but does not enforce a minimum.

Writing a controller

A controller is a Lua script that defines one global callback:

function on_tick(observation)
  -- return one of: "north", "south", "east", "west", "wait", "scan"
end

Each tick, on_tick receives a read-only observation table:

field type meaning
observation.drone.x, observation.drone.y integer the drone's current position
observation.tick integer ticks elapsed so far
observation.budget_remaining integer operational budget left before the operation runs out
observation.discovered array of tables every tile learned about so far

Each entry in observation.discovered is a table { x, y, tile, traversable, uplink }, where tile is "floor", "wall", or "hazard", traversable is whether the drone could occupy that tile, and uplink is whether it's the network-uplink objective. The drone's own tile and its four cardinal neighbours are added automatically every tick; nothing farther away is visible until discovered.

on_tick must return one of "north", "south", "east", "west", "wait", or "scan". Any other value, a move that would leave the map, or a move into a wall, ends the run with an error and does not consume budget.

A move or "wait" costs 1 budget; "scan" costs 2. "scan" does not move the drone; it reveals every tile within 2 tiles of the drone in any direction (a 5x5 area, including diagonals), regardless of walls in the way — scanning is not blocked by line of sight. Moving onto a hazard tile costs an additional 4 budget on top of the action's base cost, charged only on the tick the drone enters it; waiting on a hazard, or continuing to occupy one, costs nothing extra. Discoveries, whether from passive local vision or a scan, persist for the rest of the run. The operation fails if the budget is exhausted before the drone reaches the uplink; reaching the uplink always succeeds, even on the same action that would have exhausted the budget.

The satellite view

Before each tick's telemetry line, the console prints a compact satellite view: a grid of the terrain the drone has discovered so far, drawn north-up (highest y at the top, matching the layout table above). Tiles the drone hasn't discovered yet — through passive local vision or a "scan" — are never shown, even if they're a wall, the hazard, or the uplink.

SATELLITE FEED // discovered terrain
     x=0 x=1 x=2 x=3 x=4
y=4 |   ?   ?   ?   ?   ?
y=3 |   ?   ?   ?   ?   ?
y=2 |   ?   ?   ?   ?   ?
y=1 |   .   ?   ?   ?   ?
y=0 |   D   #   ?   ?   ?
legend: D drone   U uplink   . floor   # wall   ~ hazard   ? undiscovered
symbol meaning
D the drone's current position
U a discovered uplink tile
. discovered floor
# discovered wall
~ discovered hazard
? not yet discovered

Paste your controller into the console's Controller editor and deploy it with F6, or, for contributor/test use, run it directly with:

cargo run -- --developer-mode path/to/your_script.lua

See examples/first_contact.lua for a reference reconnaissance controller against the "First Contact" scenario. It's a small, five-rule strategy that reacts only to what it discovers and to its own memory of where it's been: remember every tile seen and every tile visited; once the uplink is discovered, move toward it using only confirmed-safe ground; otherwise, prefer a safe tile not yet visited (an arbitrary but consistent direction order breaks ties); scan once if there's no such tile; and if a scan doesn't help either, cross a known hazard or step back onto covered ground rather than stall. Deployed through the console, where F6 selects among a small set of authored configurations whose active uplink and/or hazard tile can differ from one deployment to the next, this strategy reliably solves every one of them, regardless of which of the two generic tie-break orders it uses to break its first fork.

Exit codes

Code Meaning
0 the operation succeeded — the drone reached the uplink, opening a connection into the operator network
1 the operation ran to completion and failed (e.g. ran out of budget)
2 the command itself was used incorrectly (bad flag/argument)
3 the script could not be loaded or executed (missing file, syntax error, missing on_tick, a runtime error, or an invalid action)

Reference controller and the fixed test scenario

--developer-mode always runs one fixed map, independent of the small set of authored configurations a console deployment selects via F6 (see docs/TUI_DESIGN.md's "First Contact configuration model"). This section documents that fixed map — a 5x5 facility and an 18-budget operation, the same budget shared uniformly across every authored configuration — for contributors extending or testing controllers; it is not a walkthrough for players, and studying it is not how the game is meant to be won. Tests that specifically target this fixed scenario use it directly; others in the suite exercise the other authored configurations as well.

       x=0  x=1  x=2  x=3  x=4
y=4  |  .    .    .    .    U
y=3  |  .    #    #    #    .
y=2  |  .    #    #    #    ~
y=1  |  .    .    .    .    .
y=0  |  S    #    .    #    .

S = drone start   U = uplink objective
. = floor   # = wall (impassable)   ~ = hazard (traversable; entering it costs extra budget)

None of this layout is exposed directly through the API — a controller must still discover it through observation. (2, 0) is a one-tile dead end off the shared corridor at (2, 1); it never leads anywhere, and passive discovery reveals it as soon as the drone reaches (2, 1), so a controller never needs to step into it to rule it out. Two equal-length routes also lead from the start to the uplink on this particular map: one along column x=0 and row y=4 that never touches the hazard, and one along row y=1 and column x=4 that passes through it — neither is privileged by the game, and other authored configurations move the uplink and/or the hazard elsewhere.

Contributing

The design is still taking shape. Discussion and focused proposals are welcome; see CONTRIBUTING.md before opening a pull request.

Maintainers publishing a release should see docs/RELEASING.md. Dependency updates are handled by Dependabot; see docs/DEPENDABOT.md.

License

Licensed under either of:

at your option.

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages