openbounty is a reimplementation of King’s Bounty (New World Computing, 1990) in C, built on raylib. You are a bounty hunter with a calendar, a purse, and a commission: recruit an army, hunt seventeen villains across four continents, dig up the Sceptre of Order, and get home before the days run out. The rules, the tables, the combat formulas, and the 320×200 VGA presentation all follow the original.

It runs natively on Windows, macOS, and Linux, and in the browser via WebAssembly. Beyond the original it adds a music soundtrack, MP4 gameplay recording, a data-driven pack format for total conversions, and two automated players: one that plays like a person, one that proves a world is winnable at all.
Quickstart
Play in your browser
Play it here. Nothing to install and nothing to supply, because the browser build embeds its own copy of the game data. Saves persist in your browser between visits.
This is the fastest way to see it, and the only one with no setup at all.
Download and run
Grab a build for your platform from the releases section below. There is no installer; unzip and run the binary.
- Windows: a single
.exe, statically linked. No DLLs. - macOS: a universal binary (Apple Silicon + Intel), ad-hoc signed. On first run, right-click → Open to get past Gatekeeper, or
xattr -dr com.apple.quarantine ./openbounty. - Linux: built on Ubuntu 22.04 against glibc 2.35, so it runs on most current desktop distros.
Supplying your own game data
The desktop builds ship without game data. The artwork, text, and tables are New World Computing’s and are not redistributed here; the engine is open source, the assets are not.
To play the desktop build you supply your own from a legally-owned copy of the DOS release:
./openbounty --extract /path/to/KB.EXE
That writes kings-bounty.openbounty next to the binary, and the engine picks it up on the next launch. The extractor reads the original DOS formats directly; see reversing the DOS asset formats below for what that involves.
The browser build is the exception. It has to embed the pack to run at all, so that is the one place the data travels with the engine.
Controls
Arrow keys or the numpad move you a tile at a time; the numpad diagonals work, and numpad 5 rests a day in place. The view keys toggle full-screen panels: A army, V character sheet, I contract, P puzzle grid, M minimap, C controls, O keybinding reference.
The full keybinding table, adventure and combat, is in the README.
Releases
Five artifacts are published per release: Linux x86_64, Windows x86_64 and i686, a macOS universal binary, and the WebAssembly bundle.
Version numbering
Releases are sequential build numbers under release-N tags, with no semver and no suffixes. Every push to main runs the release pipeline, which picks the next number, builds all five targets, and publishes them; the tag is only created after every build has succeeded, so a failed build leaves the number unused. The build number is compiled in and reported by --version.
The full pipeline is documented in RELEASE-PROCESS.md.
Where this came from
King’s Bounty, 1990
King’s Bounty was New World Computing’s 1990 strategy game, the direct ancestor of Heroes of Might and Magic. You recruit stacks of creatures, fight tactical battles on a small grid, and race a calendar.
OpenKB and the spec
I did not start from a disassembly. There is an earlier free reimplementation, OpenKB, an SDL 1.2 project. The route I took was to document that exhaustively first, then implement against the document.
The result is OPENKB-SPEC.md, covering every rule, data table, tile code, string offset, combat formula, spell effect, UI screen, and the byte layout of the save file.
The part that matters is that it documents OpenKB including its bugs. The broken sceptre burial, the contract bug affecting villains five through sixteen, the damage spells that were left as stubs: all recorded as they actually behave, with deviations from the DOS original called out explicitly. Some rules in that document therefore bind to the DOS binary against OpenKB, and you have to read carefully to see which is which.
This matters when implementing against it: a spec that silently corrects the behaviour it documents leaves no way to tell an intended rule from an accident.
openbounty then gets its own as-built record, OPENBOUNTY-SPEC.md, written in the present tense against what actually shipped, with a standing register of known deviations.
Reversing the DOS asset formats
The extractor is a clean reimplementation of the original file formats, and this is where the reverse engineering is concentrated:
.CCarchives: a custom container with a filename-hash table and LZW-compressed entries..4/.16/.256images: bit-packed at 1, 4, and 8 bits per pixel..CHfonts: 1bpp bitmap glyphs.KB.EXE: the executable itself is compressed, so the extractor unpacks both Execomp and ExePack before it can read the string tables and data inside.- PC-speaker tunes: frequency and duration pairs, digitised into WAV.
- MCGA palette: the 768-byte VGA palette.
That is what --extract runs. It reads a 1990 DOS distribution and produces a modern pack directory.
How it works
Engine and shell
The code splits in two. engine/ holds all game logic and knows nothing about rendering, input, audio, or windowing. src/ is the shell: raylib, drawing, input, sound.
This boundary is enforced by the build, not by convention. The engine compiles into a static library against a header-only stub of raylib, and a boundary check links it with nothing but -lm -lpthread. If any engine file reaches for a shell symbol, make fails. The same check covers the demo and autoplay agents.
The headless players, the browser port, and the test suite all link the same engine the game does.
Determinism
Given the same seed and the same sequence of inputs, a game reproduces bit-for-bit. Floating point appears only in animation and rendering, never in gameplay. Every randomised placement (zones, villains, the sceptre, dwellings, roaming foes, telecaves, artifacts, town spells) derives from the seed, so loading a save restores an identical world.
This is what lets a bug be reported as a seed number, and what makes the winnability oracle’s verdicts meaningful.
Rendering
Everything draws to a 320×200 buffer, the original VGA mode, and that buffer is scaled to the window by an integer factor, never a fractional one. Pixels stay square and sharp at any size. The font is an 8×8 bitmap and the palette is the original 256-colour VGA table.
The game-pack format
A pack is a self-contained directory, or a zipped .openbounty archive, holding everything the engine needs to run one specific game: rules, world data, art, audio, fonts, palettes.
my-pack/
├── game.json # all gameplay data + asset manifest
├── art/ # sprites, tiles, fonts, UI chrome (PNG)
├── audio/ # music + SFX (WAV / OGG)
├── maps/ # zone tile grids
└── palettes/ # 256-colour palette binaries
game.json carries the lot: troop rosters and stats, character classes, villains, spells, the world layout, economy and chest tables, the morale chart, difficulty constants, UI strings. The engine reads it at startup; nothing about King’s Bounty is compiled into the binary.
Packs are how openbounty supports re-themed games, total conversions, and community content without recompiling the engine. King’s Bounty is simply the reference pack that ships with the source.
The schema is documented in PACK-FORMAT.md.
Modding the default pack
The base King’s Bounty pack lives at assets/kings-bounty/ and is a working example to copy and change. Editing game.json retunes the game with no code and no rebuild: adjust troop stats, recruit costs, spell effects, the day budget, or the villain roster and the change takes effect on the next launch. Swap the files under art/, audio/, and palettes/ to re-skin it, or edit the grids under maps/ to change the world. Point the game at a pack directory or a .openbounty archive with --pack <path>.
For the field-by-field reference and the constraints on each asset, see PACK-FORMAT.md.
Beyond the original
Music
The DOS game had PC-speaker beeps and nothing else. openbounty keeps those. The walk, bump, chest, and defeat tunes are digitised from the originals, and it adds something the 1990 release never had: an actual soundtrack, with separate overworld and combat tracks. Both are ordinary OGG files in the pack, so a pack author can replace them by editing game.json.
Recording to MP4
--movie records your session to an MP4. The recorder captures the 320×200 render target per game tick to a temporary directory, and on exit an encoding dialog muxes it into a file. The H.264 encoder and MP4 muxer are both vendored single-header libraries, so there is no ffmpeg dependency and nothing to install.
Desktop builds only.
The automated players
openbounty ships with two agents that play the game on their own. Both live outside the engine and outside the shell, in demo/ and autoplay/, and the build enforces that they stay independent of each other. Both are desktop-only; neither is exposed in the browser.
Demo mode
--demo hands the game to an agent that plays it the way a person would. It sees only what a player sees: it works through the fog of war, cannot read the world state directly, cannot roll back a bad decision, and lives on one committed timeline. It recruits, travels, picks fights, and ends in one of the game’s own outcomes: won, lost, or stuck.
Run it visibly and it plays at a watchable pace, then hands the board back to you when it finishes. Run it with --headless and it plays to an ending with no window and prints a report. The gameplay animation at the top of this page is demo mode playing itself, recorded with --movie.
It is specified in DEMO-SPEC.md.
Autoplay: a winnability oracle
--autoplay serves a different purpose. Where demo mode plays, autoplay proves. It answers one question about a world: can it be won at all?
To answer it, autoplay does the things a player cannot. It reads the world directly instead of through the fog, and it snapshots the entire game state and rolls back to explore many lines of play before committing to one. The search is a single snapshot tree run once from boot: a node is a reached world state, and expanding it attempts exactly one objective. The expansion set is bounded and declared up front (a per-node branching cap, a frontier beam, a stagnation cut, and a runaway watchdog), so a run ends when it commits a full clear or exhausts that set.
The verdict is a statement about the pack and the seed, not about luck. Headless, it prints [VERDICT READY] and exits 0 for SOLVED, 1 for NOT-SOLVED, 2 for setup failure. SOLVED means a full clear was reached and committed. NOT-SOLVED reports the best objective count reached and means the declared search ran out; it is not a proof that the world is impossible.
You choose who the oracle plays and how hard: --autoplay-hero=<class> sets the class, --autoplay-level=<easy|normal|hard|impossible> sets the difficulty, which sets the day budget the run is proved against.
--validate-pack [LO [HI]] runs the oracle across a range of catalog worlds (default the whole 256) and prints a table: per seed the verdict, objectives cleared, days, score, moves, and time, and on a miss the first objective that blocked it and why. For a pack author this is the difference between shipping a world that is hard and one that cannot be finished. It exits 0 only if every world in the range solved.
The design is in AUTOPLAY-SPECS.md, with measured results per seed in AUTOPLAY-BASELINE.md.
Platforms
Desktop
Windows, macOS, and Linux are built from the same sources by the same makefile, cross-compiled in CI on every release. Windows is a single static .exe; macOS is a universal binary; Linux links against system libraries.
The browser build
The WebAssembly build compiles the same C, unmodified, through Emscripten.
It needed almost no restructuring. Every blocking loop in the shell, including the title menu, an entire battle, and the ending cartoon, is written as while (!frame_host_should_close()). Compiled with Emscripten’s Asyncify, that call yields to the browser, so those loops run unmodified instead of being rewritten as state machines.
Saves go to IndexedDB, so they survive a reload. The pack is embedded in the build. Recording, demo mode, and autoplay are left out of the web experience.
Command-line reference
Run with no flags to play normally. The rest cover reproducible runs, recording, the automated players, and the pack tooling. The web build ignores all of them.
Play
| Flag | Argument | Effect |
|---|---|---|
--version, -v | Print the build number and exit. | |
--help, -h | Print usage and exit. | |
--fullscreen | Start fullscreen. | |
--pack | <name|path> | Choose a pack: a bare name is resolved by discovery, a path opens a .openbounty archive or a directory holding game.json. |
--save-dir | <dir> | Override where saves and discovered packs live. |
--seed | N | Play catalog world N (0 to 255) for a reproducible run. Without it, the world is derived from the time, name, and class. Out of range is a hard error. |
--movie | [<path>] | Record the session to an MP4. With no path, writes to the user-data directory with a timestamped name. |
Automated players
| Flag | Argument | Effect |
|---|---|---|
--demo | Demo mode: the human-like agent plays at a watchable pace. | |
--autoplay | Autoplay: the winnability oracle resolves the world, then replays it. | |
--autoplay-hero | =<class> | Class the oracle plays, by pack class id (default knight). |
--autoplay-level | =<easy|normal|hard|impossible> | Difficulty the oracle plays, which sets the day budget (default normal). |
--autoplay-speed | =<slow|normal|fast> | Replay pacing for visible autoplay (default normal). |
--validate-pack | [LO [HI]] | Run the oracle over catalog worlds LO..HI (default the whole catalog) and print a winnability table. Exits 0 only if every world solved. |
--headless | With --demo or --autoplay: no window; play to an ending and exit with the verdict code. | |
--verbose | Turn on the agents’ diagnostic output. Does not change the run. |
Pack tooling
| Flag | Argument | Effect |
|---|---|---|
--extract | Build a pack from a DOS KB.EXE and exit. | |
--out-dir | <dir> | With --extract: write a loose asset tree instead of a .openbounty archive. |
--pack-dir | <src> <dst> | Zip an extracted asset tree into a .openbounty archive. |
The full flag reference, with the exit codes and the finer points of each mode, is in the README.
Documentation
The repository is documented at reproduction grade. These are the primary documents:
| Document | What it covers |
|---|---|
OPENKB-SPEC.md | The reference spec for OpenKB, documented including its bugs and its deviations from the DOS original. |
OPENBOUNTY-SPEC.md | The as-built record of this implementation, plus a standing register of known deviations. |
PACK-FORMAT.md | The pack schema: game.json keys, directory layout, asset conventions. |
DEMO-SPEC.md | Demo mode: the human-like player, its constraints and outcomes. |
AUTOPLAY-SPECS.md | Autoplay: the planner, executor, and winnability oracle. |
AUTOPLAY-BASELINE.md | Measured autoplay results, seed by seed and difficulty by difficulty. |
RELEASE-PROCESS.md | The automated release pipeline. |
The README is the practical entry point: repository layout, build environment, command-line flags, runtime data, game rules, combat, keybindings, and map formats.