I've been meaning to write about my latest project Bussard for a while now. I describe it as a "spaceflight programming adventure", but it might be easier to think of it as "Emacs in space, but with a sci-fi novella in it", written in Lua with the LÖVE engine. There's a lot to tell about the game and how I want it to eventually be a way for newcomers to learn programming, but I want to write here about a particular part of the development I had a lot of fun with.
The game is played by interacting with your ship's onboard computer. Naturally because I wanted to give the player the freedom to customize the interface as much as possible, I've modeled it on Emacs. The game starts with your ship in orbit around a star and hoping to intercept an orbiting space station, but once you poke around a bit you realize that "flight mode" is only one of many modes you can activate in your onboard computer.
Pressing ctrl-o allows you to open any file in the in-game computer, and pressing ctrl-enter opens a Lua repl buffer that uses the same editor infrastructure but lets you write code interactively and explore your ship's computer's API. Communicating with space station and planet port computers is done over an SSH client that also lives inside the editor. But all the various modes of the editor are configured with Lua code that runs in user-space; basically that code defines keyboard controls which simply invoke functions of your ship's computer to edit text, open SSH connections, engage the ship's engine, etc. Every action you can take is just a Lua function call.
-- bind is for commands that only call their functions once even when held. -- it takes the name of a mode, a key combo, and a function to run when pressed. bind("flight", "escape", ship.ui.pause) -- you can bind keys to existing functions or inline functions. bind("flight", "ctrl-return", function() ship.editor.change_buffer("*console*") end) -- the mouse wheel is handled just like any other key press. bind("flight", "wheelup", zoom_in) bind("flight", "wheeldown", zoom_out) -- regular tab selects next target in order of distance from the star. bind("flight", "tab", ship.actions.next_target)
This is a fantastically flexible model for rich interaction—it can be completely rewritten on the fly, and it's seamless to experiment with new ideas you think might support a better way of doing things. No recompiling, no restarting, just flow. But another benefit of the editor API is that you can call it against in an automated context, such as a headless run that does fuzz tests.
I had to make a few changes to the API for this to work nicely, but in the end I realized they made the system a lot more consistent anyway. The fuzz testing uncovered a nice set of nasty edge-case editor bugs that were not too difficult to fix, but would have taken a lot of time to uncover with manual testing.
local function fuzz(n) -- need to display the seed so we can replay problematic sequences local seed = tonumber(os.getenv("BUSSARD_FUZZ_SEED") or os.time()) print("seeding with", seed) math.randomseed(seed) for i=1,n do local mode = editor.mode() -- smush together all the different sub-maps (ctrl, alt, ctrl-alt) local commands = lume.concat(vals(mode.map), vals(mode.ctrl), vals(mode.alt), vals(mode["ctrl-alt"])) local command = lume.randomchoice(commands) print("run " .. binding_for(mode, command) .. " in mode " .. mode.name) try(lume.fn(editor.wrap, command)) -- sometimes we should try inserting some text too if(love.math.random(5) == 1) then try(lume.fn(editor.handle_textinput, random_text())) end end end
Of course, this is pretty limited in the kinds of bugs it can catch—only problems that result in crashes or hangs can be identified by the fuzz tests. But it gives me confidence when I make further changes if I can throw 32768 cycles of random commands at it without seeing it break a sweat. And it's even better when every incoming patch automatically has the testing applied against it using GitLab's CI.
Stay tuned for a second beta of Bussard to be released very soon! There is still a lot more I want to do with the story line and missions, but the engine is getting more and more polished with each milestone. Feedback is very welcome, as are contributions.
Update: I found that the fuzzer above has a critical flaw: it does not inspect the current mode's parent mode to look for commands there. (For instance, the console's parent mode is edit, and the ssh mode's parent is the console.) Fixing this immediately uncovered four new bugs.
 Yes, I know I just set myself up for the old "Bussard is a great OS, it just lacks a decent text editor" joke. Honestly I am just waiting for someone to come along and implement a vim mode in-game; if any player thinks they can do better than the built-in editor they are welcome to try!
 It's a bit tricky to get LÖVE to run headless, but it can be done. Mostly it involves disabling love.graphics and love.window modules in conf.lua and being careful with the order of loading. You also have to make sure that no calls to love.graphics functions happen outside your love.draw function.