Glory of Rome
Port nativo de Glory of Rome para PC.
Glory of Rome
Port nativo basado en su ficha de origen.
README del repositorio
OpenBounty
A faithful raylib reimplementation of King's Bounty (1990, New World Computing), and the engine behind The Glory of Rome, an original pack that has run on desktop, the web, iOS and Android.
This document has described how the game works, what it ships with, and how to build and run it.
Releases
Pre-built binaries have been published on the GitHub Releases page:
Every release has carried two desktop products for each of Linux x86_64,
Windows x86_64 and i686, and macOS universal, plus the web and mobile builds
(docs/RELEASE-PROCESS.md has listed the file names):
- OpenBounty (
openbounty-build-N-*): King's Bounty, the binary withassets/kings-bounty.openbountybeside it; it has started with no flags../openbounty --extracthas also built the same pack from a copy of the DOS distribution (§3). - The Glory of Rome (
gloryofrome-build-N-*): the same binary withassets/glory-of-rome.openbountybeside it; it has started with no flags. - Web / WebAssembly: two zips of
.html/.js/.wasm/.data, served over HTTP, each with its game embedded:openbounty-build-N-web-wasm.zip(King's Bounty, danheskett.com/dist/openbounty/) andgloryofrome-build-N-web-wasm.zip(The Glory of Rome, danheskett.com/dist/gloryofrome/). - iOS (
.ipa, also on TestFlight) and Android (sideload.apkand the Play.aab): The Glory of Rome only.
The Linux build has been made on Ubuntu 22.04 (glibc 2.35+). The Windows
builds have been a single .exe with no installer and no DLLs. The macOS build
has been ad-hoc signed: on first run, right-click → Open to bypass
Gatekeeper, or run xattr -dr com.apple.quarantine ./openbounty.
Releases have been sequential build numbers under release-N tags. Work has
been merged into staging, and a staging -> main pull request has been the
release: its merge has run the release workflow, which has picked the next N,
built every target, and published them; the build number has reached the user
via the archive filenames and the --version output:
./openbounty --version # → openbounty build 3
The number has been derived from the most recent release-N git tag (0 when
there are none). Override with OPENBOUNTY_VERSION=N on the make command
line. docs/RELEASE-PROCESS.md has been the
maintainer's release procedure.
1. Repository layout
The codebase has split into engine (game logic, raylib-free, built as
libobengine.a) and shell (renderer, audio, input, screens, linking the
engine library). The two binaries (openbounty for play and
openbounty-test for tests) have both linked the same engine archive. The
shell has reached the platform only through five seams (src/gfx.h,
src/frame_host.h, src/input_host.h, src/audio_backend.h,
src/font_backend.h): raylib has implemented them everywhere except iOS,
where ios/ has implemented them natively (docs/IOS-BACKEND.md).
.
├── Makefile # Every target: desktop, web, Android, iOS, dist
├── run.sh # `make && ./build/debug/openbounty`
├── art.html # Review page for the Rome art, refreshed from disk
├── newart.html # Review page for the newer Rome art, in context
│
├── engine/ # ENGINE: pure game logic -> libobengine.a
│ ├── README.md # Architecture, file map, linking instructions
│ ├── include/ # Public API (consumers add -Iengine/include)
│ ├── headless/ # raylib stub headers for headless consumers
│ └── *.c # game, map, fog, step, combat, flows, pack,
│ # savegame, resources, player_io, ...
│
├── src/ # SHELL: renderer + audio + input + screens
│ ├── main.c # CLI parsing, init sequence, main loop
│ ├── shell_*.c # Main-loop pieces: actions, menu, prompts,
│ │ # cheats, gate, gallery, demo/autoplay adapters
│ ├── gfx_raylib.c, frame_host.c, input_host.c, audio_raylib.c, font_raylib.c
│ │ # The raylib side of the five seams
│ ├── layout.c, present.c # Buffer, scale and viewport (REQ-528)
│ ├── touch.c, textsel.c # Touch regions and the on-screen keyboard
│ ├── combat_loop.c, combat_render.c, combat_replay.c
│ ├── startup.c # Splash, title, credits, class, name
│ ├── plat_android.c, plat_ios.c, safe_area.c
│ ├── modern/ # Modern-mode panels, menus and location screens
│ ├── legacy/ # Legacy-mode draw layer, frozen
│ ├── screens/ # Shared location-screen flows
│ └── recorder.c, encode_*.c # --movie capture and the MP4 encoder
│
├── demo/ # Demo mode, the human-like player (DEMO-SPEC)
├── autoplay/ # Autoplay, the winnability oracle (AUTOPLAY-SPECS)
├── ios/ # Native iOS backend: Metal, AVAudioEngine, UIKit
├── android/ # Manifest, activity, icons, Play listing
├── web/shell.html # The WebAssembly page template
│
├── assets/
│ ├── kings-bounty/ # The reference pack, extracted from KB.EXE
│ └── glory-of-rome/ # The Glory of Rome pack
│
├── art/ # Rome art sources: generation jobs, map sources,
│ # terrain primitives, references
├── tests/ # unit/, e2e/, regression/, autoplay/, library/
├── third_party/ # cJSON, miniz, greatest, minih264, minimp4, vo-aacenc, stb,
│ # Liberation Sans; raylib-install*/ (built)
├── tools/ # C pack extractor; Rome art and map tools (Python)
├── scripts/ # raylib builds, store and release scripts
├── dist/ # Release README templates
├── docs/ # Specs and procedures (see below)
└── legacy/bin/ # Default extractor input (not in git)
docs/: OPENBOUNTY-SPEC.md (the reproduction-grade spec), PACK-FORMAT.md,
DESIGN-SPEC.md, ART-SPEC.md,
ART-PIPELINE.md, ROME-ART.md (every prompt, generated),
GLORY-OF-ROME.md, DEMO-SPEC.md, AUTOPLAY-SPECS.md,
AUTOPLAY-BASELINE.md, IOS-BACKEND.md, RELEASE-PROCESS.md,
STORE-SUBMISSION.md, and OPENKB-SPEC.md (the predecessor project).
2. Build environment
raylib has not been committed. Each scripts/build_raylib_<platform>.sh has
cloned raylib (pinned to 6.0) and installed its headers and libraylib.a
into a gitignored third_party/raylib-install*/ directory. Run the script
for your platform once on a fresh clone; CI has run them before every build.
Bump all five together when moving raylib versions.
Linux (development)
Requirements:
gcc(C99)- raylib built once:
./scripts/build_raylib_linux.sh - System libs:
pthread,dl,rt,X11,m
Compile flags: -std=c99 -Wall -Wextra, with -O0 -g for make and
-O2 -DNDEBUG for make release; the engine archive -O2 -fPIC -DOB_HEADLESS in both.
./scripts/build_raylib_linux.sh # once, on a fresh clone
make # debug build -> build/debug/openbounty (-O0 -g)
make run # build + run (debug)
make release # optimized + portable build -> build/release/openbounty (-O2)
make run-release
make test # build + run the test suite
make clean # rm -rf build
Windows (cross-compile from Linux)
Requirements:
x86_64-w64-mingw32-gccandi686-w64-mingw32-gcctoolchains- raylib built once:
./scripts/build_raylib_windows.sh(installs boththird_party/raylib-install-win64/and-win32/)
make windows # builds build/openbounty-x64.exe and build/openbounty-x86.exe
They have linked statically (-static -static-libgcc -mwindows), no DLLs.
Like every desktop build, the .exe has read its pack from discovery (§4,
Asset paths).
Web (WebAssembly, via Emscripten)
Requirements:
- The Emscripten SDK on
PATH(emcc). Locally:git clone https://github.com/emscripten-core/emsdk third_party/emsdk, thenemsdk install latest && emsdk activate latestandsource third_party/emsdk/emsdk_env.sh. CI has usedsetup-emsdk. - raylib built once:
./scripts/build_raylib_web.sh
make web # both packs -> build/web/<pack>/openbounty.{html,js,wasm,data}
make web-glory-of-rome # one pack only
make web-kings-bounty
make web-serve # build both + serve on http://localhost:8080
A wasm module has embedded its pack, so there has been one build per pack,
each in its own build/web/<pack>/. The pack has gone into openbounty.data
via --preload-file, so each build has been self-contained. All four files
have been needed, served over HTTP: browsers have refused to fetch .wasm/.data
over file://. Saves have persisted in IndexedDB. make web has not been
part of make dist; make dist-web has packaged each build as its own zip,
openbounty-* for King's Bounty and gloryofrome-* for The Glory of Rome, and
the site has served each at its own URL.
Android
./scripts/build_raylib_android.sh --ndk <ndk> # raylib 6.0, arm64-v8a static
make android ANDROID_NDK=<ndk> ANDROID_SDK_ROOT=<sdk> # -> build/gloryofrome.apk
make android-play ANDROID_NDK=<ndk> ANDROID_SDK_ROOT=<sdk> # -> the upload-signed .aab
The NDK and SDK paths have come only from the command line: the script has
taken --ndk, --api and --arch, and the Makefile has stopped with an
error when ANDROID_NDK or ANDROID_SDK_ROOT arrives through the
environment.
Mobile has shipped The Glory of Rome only -- one pack, bundled in the APK's
assets/, opened by src/plat_android.c (pack_open_mem, so it has not
depended on raylib's fopen wrap, which miniz can bypass via fopen64).
There has been no pack discovery and no picker. Saves have gone to the app's
private directory.
The app has been a NativeActivity (no Gradle): javac -> d8 -> aapt ->
zipalign -> apksigner, all from the Makefile. It has been
landscape-locked, and the display-cutout insets have been pushed from the
Activity into src/safe_area.c, which present_scaled has used to fit and
centre the game clear of the camera and the gesture bar. The system Back
gesture has answered wherever the shell reads Escape (src/input_host.c).
The launcher icon has been android/res/mipmap-xxxhdpi/ic_launcher.png,
composed from the pack's own title art by tools/romeart.py icon.
iOS
make mac # the pack tool for macOS
make ios-sim PACK_TOOL=build/openbounty-mac # Simulator .app
make ios PACK_TOOL=build/openbounty-mac # device .ipa (signed when IOS_SIGN_IDENTITY,
# IOS_PROFILE and IOS_TEAM_ID are set)
iOS has drawn with its own Metal backend (ios/gfx_metal.mm) -- there has
been no raylib in the build -- behind the same seams every platform has used
(src/gfx.h, src/frame_host.h, src/input_host.h, src/audio_backend.h,
src/font_backend.h). The game has run on its own thread with its loops
intact; UIKit and a CADisplayLink have run on the main thread. make has
type-checked every iOS-bound shell file with -DPLATFORM_IOS and no raylib
include path (build/ios-purity.stamp), so the split has held on machines
without Xcode. See docs/IOS-BACKEND.md.
Like Android, iOS has shipped The Glory of Rome only: the pack has been a
bundle resource opened with pack_open_mem, and saves have gone to the app's
Documents/saves. All iOS building has happened on GitHub's macOS runners.
Assets at run time
Every build has read its game data from a pack -- a .openbounty zip, or a
loose directory holding game.json -- found by discovery (§4, Asset paths).
Engine-side byte reads have gone through LoadAssetBytes
(engine/assets_bytes.c) and shell-side texture loads through
LoadAssetTexture (src/assets.c); the engine has never touched a GPU
texture.
3. Command-line flags
The build has produced two binaries, each with its own CLI surface, the game
(build/debug/openbounty) and the test runner (build/openbounty-test), plus
a compile-only library boundary check that has emitted no binary.
build/debug/openbounty, the game
The game binary has covered normal play plus the tool modes (extractor, pack builder, gallery, demo, autoplay and pack validation).
| Flag | Argument | Effect |
|---|---|---|
--version, -v | - | Has printed openbounty build <N> and exited. |
--help, -h | - | Has printed usage and exited. |
--fullscreen | - | Has toggled fullscreen after window creation. |
--pack | <name|path> | Has selected a pack: a bare name has resolved via pack discovery, a path has opened a .openbounty zip or a directory containing game.json. |
--lang | <code> | Has loaded the pack's strings/<code>.json instead of its base language (world.language, default en), falling back to the base file when that one is absent. A missing key has been a hard error. |
--save-dir | <dir> | Has overridden the user data directory (where saves and discovered packs have lived). |
--seed | N | Has selected catalog world N (0–255) for a reproducible run. A pack has held 256 worlds; the index has been expanded to a full-width RNG seed internally (§11). Out of range, negative, or unparseable has been a hard error (exit 2): nothing has run on a misunderstood command line. Without --seed, a world has been derived from time + name + class. |
--movie | [<path>] | Has recorded gameplay to an MP4: with no argument to <user-data>/movie-<timestamp>.mp4, with a path there. At shutdown an "Encoding…" dialog has run the muxer; intermediate per-tick frames have lived in /tmp/openbounty-movie-<pid>, deleted afterward. The file has been written only on a clean shutdown. |
--debug | - | Has added the Debug page of cheats to the game menu. Without it no cheat has been reachable. |
--gallery | <dir> | Has captured every modern screen to <dir>/<name>.png and exited: views, prompts, dialogs, town and castle pages, menus, combat. No input has been read. Each capture has also checked that a tap reaches the rows the screen drew; a failed check has printed [tapcheck] FAIL and made the exit code non-zero. |
--intro-movie | <out.mp4> | Has rendered the pack's Introduction offline to <out.mp4> at 15 frames a second, with its sound, printed its cue sheet (each scene's start and length) and exited. |
--puzzle-sweep | <dir> | Has captured the puzzle view of every catalog world, the centre piece lifted, to <dir>/puzzle_<NNN>_<zone>_<x>_<y>.png (the world's index and its scepter) and exited. |
--window | WxH | Has opened the window at that size, a device's surface on the desk. |
--touch | - | Has made the session a touch device: touch-sized rows and the letter grid for the name. |
--demo | - | Demo mode: the human-like player agent (demo/, docs/DEMO-SPEC.md) has played the live game at a watchable pace. With --headless, it has played to an ending with no window and printed the [DEMO OVER] report (exit 0 = WON). |
--autoplay | - | Autoplay, the pack-winnability oracle (autoplay/, docs/AUTOPLAY-SPECS.md). One mechanism: a single snapshot-tree search run once from boot, where a node has been a reached world state and one expansion has attempted exactly one objective. Greedy has not been a separate stage: it has been the tree's first descent. The expansion set has been bounded and declared (per-node branching cap, frontier beam, stagnation cut, runaway watchdog), so a run has ended when it commits a clear or exhausts that set. Visible mode has resolved headlessly then replayed on the live world. With --headless, it has driven to its verdict and printed [VERDICT READY] (exit 0 = SOLVED, 1 = NOT-SOLVED, 2 = setup failure). SOLVED has meant a full clear reached and committed; NOT-SOLVED has reported the best objective count reached and meant the declared expansion set ran out, not that the seed is unwinnable. |
--autoplay-hero | =<class> | Modifier for --autoplay / --validate-pack: the class the oracle plays, by pack class id (default knight). A class the pack does not define has been a hard error. |
--autoplay-level | =<easy|normal|hard|impossible> | Modifier for --autoplay / --validate-pack: the difficulty the oracle plays (default normal). The level has set the day budget the run is proved against, via the pack's time.days_per_difficulty. |
--autoplay-speed | =<slow|normal|fast> | Modifier for visible --autoplay: replay pacing (default normal). No effect headless. |
--validate-pack | [LO [HI]] | Pack-author winnability report: the headless oracle has run over catalog worlds LO..HI (default 0..255, the whole catalog), one seed at a time, and printed a table: per seed the verdict, objectives cleared, days, score, moves, and elapsed time, and on a miss the first objective that blocked it and why. A totals row has closed with PASS/FAIL and per-seed averages. Exit 0 only when every seed in the range has solved. With --verbose, every diagnostic channel has printed too, and each row has been followed by its seed's [SIEGE] lines (one per villain siege on the committed line). |
--headless | - | Modifier for --demo / --autoplay: no window has opened; the run has played to its ending and exited with the mode's verdict code. |
--verbose | - | Has turned on the agent diagnostic channels (all at once; there has been no per-channel selection). Observation-only: a gated-off run has been bit-for-bit identical. |
--extract | - | Has built the King's Bounty pack from a DOS distribution (KB.EXE, 256.CC, 416.CC) and exited. It has taken no path: input legacy/bin/ when it holds KB.EXE, else the current directory; output <user-data>/<pack_id>.openbounty, the same as the shipped pack, with the music copied from an installed King's Bounty pack when there is one. |
--out-dir | <dir> | Modifier for --extract: a loose asset tree to <dir> instead of a zip. |
--pack-dir | <src> <dst> | Has zipped a pre-extracted asset tree into a .openbounty archive. The Makefile has used it to build the shipped packs. |
Normal play: ./build/debug/openbounty (no flags). The headless modes and
--validate-pack have taken --pack; without it they have opened the loose
assets/kings-bounty tree of a source checkout, not a discovered pack.
build/openbounty-test, test runner
Driven by the greatest single-header framework, layered:
| Layer | What it has covered |
|---|---|
unit (tests/unit/) | Single-function or small-scope state checks: combat math, RNG, map/fog/tile state, table lookups, JSON serialization, player-IO queue, layout, touch. |
regression (tests/regression/) | Pinned golden outputs: combat-formula digests, save-file fixture round-trips. A failure has meant behavior changed; investigation has decided intent vs bug. |
e2e (tests/e2e/) | Multi-step flows across systems: game flow, chest, contract, economy, score, combat input, save round-trips, a pack with no content limits. |
autoplay (tests/autoplay/) | The oracle's determinism plumbing: world snapshot/rollback bit-identity and the recording sink's fingerprints and mark/rollback. |
Suite names have carried their layer as a prefix (unit_terrain_suite,
regression_combat_digests_suite, e2e_game_flow_suite,
autoplay_worldsnap_suite), so greatest's -s substring filter has selected
a layer.
| Flag | Argument | Effect |
|---|---|---|
-h, --help | - | Print usage and exit. |
-l | - | List all suites and tests (dry run), then exit. |
-f | - | Stop the runner after the first failure. |
-a | - | Abort on first failure (implies -f). |
-v | - | Verbose output (per-test status, timings). |
-s | SUITE | Only run suites whose name contains the substring. |
-t | TEST | Only run tests whose name contains the substring. |
-e | - | Require exact-name match for -s / -t instead of substring. |
-x | EXCLUDE | Exclude tests whose name contains the substring. |
Examples:
./build/openbounty-test # run everything
./build/openbounty-test -s unit_ # the unit layer
./build/openbounty-test -s regression_ # the regression layer
./build/openbounty-test -s e2e_ # the e2e layer
./build/openbounty-test -s autoplay_ # the autoplay layer
./build/openbounty-test -s combat_ # all combat (any layer)
./build/openbounty-test -t damage -v # one test, verbose
./build/openbounty-test -l # list everything
./build/openbounty-test -f # stop on first failure
Library boundary check (no binary emitted)
make all has run a compile-only verification: tests/library/consumer.c +
engine/host_noop.c + libobengine.a linked with only
-Iengine/headless -Iengine/include (no -Isrc) and only -lm -lpthread (no raylib, no X11). The output binary has been discarded;
build/libtest-pass.stamp has been touched on success. If the engine starts
depending on shell headers or shell symbols, this build step has failed and
make all has failed. There has been no runtime test binary.
Recap of make targets
| Target | What it has done |
|---|---|
make / make all | Built build/debug/openbounty + libobengine.a + pack zips + library boundary check + iOS purity check + the touch and page guards. |
make test | Run build/openbounty-test via greatest: the unit, regression, e2e and autoplay layers, including the combat-formula golden digests. |
make release | build/release/openbounty, -O2 -DNDEBUG, libgcc linked statically. |
make windows | Cross-compiled openbounty-x64.exe and openbounty-x86.exe. |
make windows-debug | The same with a console attached for stderr. |
make mac | openbounty-mac (universal binary, on macOS). |
make web | WebAssembly build, one per pack -> build/web/<pack>/openbounty.{html,js,wasm,data} (needs emsdk). |
make web-serve | Built both web builds + served them on http://localhost:8080/<pack>/openbounty.html. |
make android / make android-play | The Glory of Rome APK (build/gloryofrome.apk) / the upload-signed AAB (needs the Android NDK + SDK build-tools). |
make ios-sim / make ios | The Glory of Rome Simulator .app / device .ipa (on macOS). |
make extract | Wrapper for ./build/debug/openbounty --extract. |
make extract-pack | Regenerated assets/kings-bounty/ from a user's DOS files. |
make dist-{linux,windows,mac} | OpenBounty distribution archives. |
make dist-rome-{linux,windows,mac} | The Glory of Rome distribution archives. |
make dist-web, make dist-android, make dist-android-play, make dist-ios | The two web zips and the mobile packages in dist/. |
make clean | Removed build/ and dist/ archives. |
4. Runtime data
Window and rendering
- Legacy mode (King's Bounty): the render target has been 320×200
(
CL_SCREEN_W/H, the original VGA mode), integer-scaled to fit the window preserving aspect ratio, minimum 2×. The base window has been 640×400 (CL_WINDOW_W/H=CL_SCREEN_W/H×CL_SCALEinsrc/layout.h). - Modern mode (The Glory of Rome): the pack has declared a buffer (800×504
for Rome), the smallest screen, not a fixed size. The scale has been the
largest whole number (3 at most) at which it fits the surface -- there has
been no zoom setting, and a desktop window has held its scale while an
edge is dragged -- and what the surface leaves over at that scale has gone
to the map; every page has kept its size (
docs/DESIGN-SPEC.md). The desktop window has opened at the declared buffer times the largest whole scale the monitor allows. - The window has been resizable. Fullscreen toggle: Alt+Enter.
- 256-color palette loaded from the pack's
palettes/palette.bin(for King's Bounty, extracted from the original DOS MCGA.DRV). - King's Bounty's font has been
art/font/kb-font.png(8×8 bitmap,src/bfont.c); The Glory of Rome has declared a TrueType face (docs/PACK-FORMAT.md§2.2). - King's Bounty's map tiles and sprites have been 48×34 pixels; Glory of
Rome's have been 96×96 (
docs/ART-SPEC.md).
Save files
- One save format: JSON, version 11 (
SAVE_VERSIONinengine/include/savegame.h). Catalog references have used string IDs (e.g."troop": "knights"). A catalog game has stored its world index (seed_index) rather than the expanded seed, so the world has reloaded exactly (§11). - Save slots: 10 (
SAVE_SLOT_COUNTinengine/include/savepath.h); the modern Save and Load pages have offered the first five (MODERN_SAVE_SLOTS,src/modern/saveslots.h). - Save files:
<user-data>/saves/<pack_id>/save_0.dat…save_9.dat, one directory per pack (engine/savepath.c), where<user-data>has been:- Linux:
$XDG_DATA_HOME/openbountyif set, else~/.local/share/openbounty. - Windows:
%APPDATA%\OpenBounty. - macOS:
~/Library/Application Support/OpenBounty. - Web:
/saves, an IndexedDB-backed mount the page has synced.
- Linux:
--save-dir <dir>, iOS (Documents/saves) and Android (the app's privatesavesdirectory) have used a flat directory with no per-pack level.- Fog of war has been encoded as 4 tiles per hex nibble (1 bit per tile).
- The scepter location has been stored in plaintext.
Asset paths
A game's data has been a pack: a .openbounty zip, or a directory
holding game.json. --pack <name|path> has named one; otherwise discovery
has walked, in order, the .openbounty zips in the working directory, in
<user-data>, and in <exe>/assets/ (engine/pack.c pack_discover). One
pack found has opened directly; several have opened the pack picker; none has
fallen back to a first-run extraction from KB.EXE in legacy/bin/ or the
working directory (src/main.c). Paths inside a pack have been relative to
its root and read through LoadAssetBytes / LoadAssetTexture.
5. Game configuration (assets/kings-bounty/game.json)
All gameplay data has been JSON-driven, and docs/PACK-FORMAT.md has been
the one reference for every key, its type and its default. King's Bounty's
game.json has carried the top-level keys title, version, pack_id,
pack_name, pack_kind, render, world, time, economy, tuning,
combat, zones, towns, castles, troops, spells, classes,
villains, artifacts, tile_codes, spawn, contract, controls,
sprites, ending, colors, credits and audio.
Every user-visible string has lived beside game.json in strings/en.json
(rank titles, ending text, audience dialogs, etc.).
Compile-time caps (in engine/include/game.h)
GAME_NAME_LEN = 16GAME_ARMY_SLOTS = 5
Everything else -- towns, castles, zones, consumed tiles, dwellings,
placements, foes, the catalogs -- has been sized from the pack at load
(tests/e2e/test_no_limits.c).
6. Game rules and mechanics
The rules below have been King's Bounty's; The Glory of Rome has renamed them and
kept the numbers (docs/GLORY-OF-ROME.md).
Classes (4)
| ID | Name | Starting gold | Starting army |
|---|---|---|---|
knight | Knight | 7500 | 20 Militia + 2 Archers |
paladin | Paladin | 10000 | 20 Peasants + 20 Militia |
sorceress | Sorceress | 10000 | 30 Peasants + 10 Sprites |
barbarian | Barbarian | 7500 | 20 Wolves |
Each class has had 4 ranks. Each rank has defined leadership,
commission, villains_needed, max_spells, spell_power. Ranks have been
reached by audience with the king after capturing the required number of
villains.
Difficulties
| ID | Days | Score multiplier |
|---|---|---|
| Easy | 900 | ×0.5 |
| Normal | 600 | ×1 |
| Hard | 400 | ×2 |
| Impossible | 200 | ×4 |
Days have ticked down by 1 per day_steps (40) overworld steps. Weeks have
been week_days (5) days. Day rollover has advanced days_left; week
rollover has fired the end-of-week sequence (astrology + budget).
Score formula
score = 500 × villains_caught + 250 × artifacts_found
+ 100 × castles_owned − followers_killed
then halved on Easy, or multiplied by 1 / 2 / 4 on Normal / Hard /
Impossible, and never below 0
Implemented in GameComputeScore (engine/game.c) from the pack's
economy.scoring; docs/OPENBOUNTY-SPEC.md REQ-400 has given the rule.
Movement
- One step per keypress (no auto-repeat).
- Arrow keys + numpad 1–9 + Home/End/PgUp/PgDn for 8-direction movement.
- Numpad 5 (and, in modern mode, the number-row 5) = "rest one day" (it has consumed a step, with no movement).
- Walking on land, sailing in boat (water + bridges only), flying with Mount=Fly has bypassed ground/water restrictions and skipped interactive tiles.
- Stepping onto an interactive tile has triggered its handler. Most interact tiles have bounced the hero back to the previous square (castles, towns, dwellings, the alcove, some foes); chests / artifacts / signs / orbs / navmaps / telecaves have not bounced.
- Desert tiles have consumed the entire day's step budget (one tile per day).
Foes
- Foes have occupied single tiles with a
wandering_armysprite. - Two flavors per
FoeState.friendly:- Friendly: stepping on has offered the troop to the army, free of charge.
- Hostile: stepping on has bounced back and prompted attack.
- Foe sources:
- Static foes from each zone's
wandering_armies[]JSON array, registered as hostile at GameInit. - Salted friendly foes from
salt_continentplaced on random chest slots.
- Static foes from each zone's
- Hostile foes have chased the hero each step (see "foes_follow" below).
- Friendly foes have shared the same follow logic (one foe table, classification by flag).
foes_follow
Each overworld step has called GameFoesFollow. For every foe within 2
tiles of the hero's previous position (last_x, last_y), it has:
- Evaluated all 9 cells of the foe's 3×3 neighborhood.
- Scored each cell by Euclidean distance to the hero's previous position.
- Given non-center cells that are unwalkable (or another interactive) a sentinel max-distance score, so they have never been picked.
- Not excluded the hero's current tile: a foe landing on the hero has been the combat trigger.
- Moved the foe to the lowest-score cell.
- Returned the foe's index when the chosen cell is the hero's tile, so the caller has fired the attack/recruit flow; otherwise updated the map tile and cleared the previous tile.
A static guardian has never moved. A foe has never stepped onto another foe, onto desert, a bridge, a town or any other interactive tile, or into the approach of a castle gate, and a flying hero's tile has been skipped.
Salting (salt_continent)
At GameInit, for every continent, the engine has:
- Registered every
wandering_armies[]entry as a hostile foe with a rolled garrison (using the per-continent tier spawn table). - Built a "barrel" of all chest slots in that zone.
- Tagged random barrel slots with the salt budget kinds: artifacts, navmaps, orbs, telecaves, dwellings, friendly foes.
- Created a
SaltedPlacement(orFoeStatefor friendlies) for each tagged slot. Friendly foes have used the sameg->foes[]table as hostiles. - Left
MapLoadZoneWithPlacementsto stamp these onto the loaded map.
Day/week tick (GameOnStep → end_day)
On each step the engine has:
- Decremented
time_stopand skipped the day tick, iftime_stop > 0. - Zeroed
steps_left_todayon desert terrain, else decremented it. - Run
end_daywhilesteps_left_today <= 0, which may fire week rollover. - At week rollover: paid commission, deducted upkeep + boat rental, kept the boat however short the purse, rolled the astrology troop, refilled its dwellings and grown its troops in enemy garrisons and wandering armies. Player castles have not been repopulated, and dwellings have not grown at any other time.
Spells
14 spells split into combat and adventure:
| Combat | Adventure |
|---|---|
| Clone (2000) | Bridge (100) |
| Teleport (500) | Time Stop (200) |
| Fireball (1500) | Find Villain (1000) |
| Lightning (500) | Castle Gate (1000) |
| Freeze (300) | Town Gate (500) |
| Resurrect (5000) | Instant Army (1000) |
| Turn Undead (2000) | Raise Control (500) |
Adventure spells have been cast via U on the overworld; combat spells have
fired from the in-combat spells menu.
Adventure spell effects:
- Bridge: has prompted for direction and placed 2 bridge tiles on water.
- Time Stop: has added
spell_power × 10steps totime_stop. Steps duringtime_stophave not ticked the day. - Find Villain: has marked the active villain's castle as known on the map.
- Castle Gate / Town Gate: have prompted for a visited castle/town and teleported the hero there.
- Instant Army: has spawned a troop slot.
- Raise Control: has bumped
leadership_current.
Artifacts (8)
| ID | Effect |
|---|---|
| Sword of Prowess | +50% army damage |
| Shield of Protection | -25% army damage taken |
| Crown of Command | Doubles leadership |
| Articles of Nobility | Increases weekly commission |
| Amulet of Augmentation | Doubles spell power |
| Ring of Heroism | Doubles max spells carried |
| Book of Necros | No effect (the original's unimplemented slot, REQ-333) |
| Anchor of Admirability | Boat rental costs 100 instead of 500 |
Powers have been applied on pickup in GameClaimArtifact. Querying with
GameHasPower(g, ARTIFACT_POWER_*) has resolved the runtime effect (e.g.
boat_cost).
Win and lose conditions
- Win: find the buried scepter (use Search on its tile). It has
triggered
show_win_game(cartoon + win text dialog). - Lose:
days_leftreaches 0. It has triggeredshow_lose_game.
Out-of-game flows
- Audience with the King (visit home castle): if the villain count has reached the next rank's threshold, the hero has been promoted, through as many ranks as the count reaches. Otherwise: "I can aid you better after you've captured N more villains."
- Recruit at home castle: per-troop quantity prompt with leadership / gold checks.
- Garrison castle (own non-home castle): swap troops between army and garrison.
- Siege castle (enemy): with siege weapons, "Lay siege (y/n)?" ->
has dispatched to the combat module (
engine/combat.c+src/combat_loop.c); without them the gate has bounced the hero. - Visit town: A) New contract / B) Rent boat (500 / 1 week) / C) Gather information / D) Buy spell / E) Buy siege weapons (3000).
- Visit dwelling: numeric prompt for troop count, capped by population and player leadership.
- Visit telecave: has teleported to the paired telecave.
- Visit alcove (Aurange): pay 5000 gold to learn magic.
- Read sign: has opened a flavor dialog.
- Open chest: has rolled one of (gold-or-leadership / commission boost / spell power / max spells / new spell / empty); special outcomes for navmap / orb tiles.
7. Combat
Turn-based tactical combat on a 6×5 grid. Split:
- Engine half (
engine/combat.c): state, AI, headless turn loop, damage formula, combat spells. No raylib. It has exposed theCombatstruct,combat_init,combat_ai_action,combat_run_headless,combat_test_digest, thespell_*helpers. - Shell half (
src/combat_loop.c):RunCombat+ modal player input + target picker + per-frame present. It has used raylib. - Renderer (
src/combat_render.c): has drawn the battlefield, the units and their count badges, and the banner.
Player input has included movement, wait/skip (Space/W), shoot (S), fly
(F, only when the active unit has TROOP_ABIL_FLY), use magic (U), give up
(G), and the controls/options/army/character views (§8, Combat). In modern
mode every one of them but Options has also been a row in the combat menu
(docs/DESIGN-SPEC.md), which has opened on its Actions page with the cursor
on Unit (DSGN-0119).
The combat-formula golden digests have lived in
tests/regression/test_combat_digests.c and run via make test.
8. Keybindings
Adventure mode (overworld), then combat and dialogs. In modern mode every
action has also been a menu row or a tap (docs/DESIGN-SPEC.md).
Movement
| Key | Action |
|---|---|
| ↑ / Numpad 8 | Move N |
| ↓ / Numpad 2 | Move S |
| ← / Numpad 4 | Move W |
| → / Numpad 6 | Move E |
| Numpad 7 / Home | Move NW |
| Numpad 9 / PgUp | Move NE |
| Numpad 1 / End | Move SW |
| Numpad 3 / PgDn | Move SE |
Numpad 5 (and 5 in modern) | Rest one day in place |
Views (toggle on/off)
| Key | View |
|---|---|
A | Army (per-troop stats) |
V | Character (sheet) |
I | Contract |
P | Puzzle (5×5 reveal grid) |
M | Worldmap minimap |
C | Controls (settings) |
O | Options (legacy); the game menu (modern) |
Actions
| Key | Action |
|---|---|
S | Search current tile (10 days) |
U | Use magic (cast adventure spell) |
W | Wait until end of week |
F | Fly (mount) |
L | Land (dismount) |
D | Dismiss army |
N | New continent (sail) |
G | Goto (modern): pick a seen tile on the world map, and travel there |
Meta
| Key | Action |
|---|---|
Q | Legacy: save to slot 0 at once, then a message where Ctrl+Q quits and any other key continues. Modern: the game menu on its Save page |
Ctrl+Q | Quit without saving, after a yes/no (on an open message too; on the legacy Q save message, at once) |
| Alt+Enter | Toggle fullscreen |
| Backtick (`) | Manual screenshot to screenshots/shot_NNNN.png |
| Esc | Dismiss view / cancel prompt; in modern mode, open the game menu |
Gamepad
On the map; a keyboard action in the same frame has won.
| Button | Action |
|---|---|
| D-pad / left stick | Move |
| A | Search |
| X | Use magic |
| Y | Wait until end of week |
| LB | Army |
| RB | Character |
| LT | Fly |
| RT | Land |
| Start | Worldmap |
| Back | Options (legacy); the game menu (modern) |
| B | Cancel, where a screen reads it (Controls, Goto, the bridge, the quit question) |
In menus, lists and questions, and in combat, the gamepad has answered as the keys do: the d-pad or stick as the arrows, A as Enter, B as Escape. In combat the d-pad moves the unit or the target cursor, A confirms a target, and B opens the action menu (modern), backs out of it, or cancels a target.
Combat
| Key | Action |
|---|---|
| Arrows / numpad / Home, End, PgUp, PgDn | Move the active unit (8 directions) |
Space / W / Numpad 5 | Wait (skip the unit's turn) |
S | Shoot: pick an enemy |
F | Fly: pick an empty cell (flyers only) |
U | Use magic: a lettered spell menu, A–G (legacy); the combat menu's spells page (modern) |
G | Give up (yes/no) |
A | Army view |
V | Character view |
C | Controls |
O | Options (legacy) |
Enter / Space / A / C | Confirm the picked cell (shoot, fly, spell target) |
| Esc | Cancel a pick; close a view; in modern mode, open the combat menu or go back a page |
Dialog dismiss
| Key | Action |
|---|---|
| Any non-modifier key | Advance / dismiss a message |
Ctrl+Q | Close the message and ask to quit without saving (on the legacy Q save message, quit at once) |
Y / N | Yes / No on yes-no prompts |
1–5 (or numpad) | Choice on numbered prompts |
A / B | Choice on two-choice prompts |
| Digits / Backspace | Type a count (legacy) |
| Enter | Yes on a yes-no prompt and commit a count (legacy); act on the cursor's row (modern) |
| Space | Act on the cursor's row, commit a count (modern) |
| Arrows | Move the cursor; step a count by 1 (←/→) or 10 (↓/↑) (modern) |
| Esc | Cancel / dismiss |
9. UI conventions
Legacy mode's conventions follow; modern mode's panels have been in
docs/DESIGN-SPEC.md.
Adventure HUD
- Top status bar:
Options / Controls / Days Left:NNN, swapped forPress 'ESC' to exitwhile a view or dialog is open. - Map viewport: 5×5 tiles (240×170) with the hero centered. The camera has clamped at zone edges.
- Right sidebar: 48px wide. It has shown the portrait, the contract/siege/magic/puzzle icons and the gold counter.
- Bottom: has dropped out for dialogs and prompts.
Dialogs
Two flavors:
open_dialog(header, body)(src/ui.c): bottom-frame box with an optional header line + multi-line body, dismissed by any key, paged by the renderer's wrap when long.prompt_yes_no_open/prompt_numeric_open/prompt_text_input_open(src/prompt.c): these have blocked input until the prompt resolves; the result has been dispatched via thepending_flowstate machine insrc/shell_promptdispatch.c.
View screens
Full-screen overlays with a yellow border, drawn on top of the overworld.
The status bar has read Press 'ESC' to exit.
10. Map data format
Maps have been ASCII files at maps/<zone>.dat in the pack.
- Lines starting with
#have been comments. - Each subsequent line has been one row (y=0 at top).
- Each character has been one tile, mapped via
tile_codesingame.json(0x20..0x7EASCII chars have corresponded to tile codes). - Width and height have come from each zone's
width/heightJSON fields: 64×64 for every King's Bounty zone; The Glory of Rome's have been 64×128, 64×64, 64×28 and 64×44.
Tile codes have carried: art name, terrain category (grass / forest /
mountain / water / desert / river), blocks_foot flag, is_bridge flag.
Walkability: terrain in {grass, desert} OR is_bridge OR
interactive != INTERACT_NONE (interactive tiles have overridden terrain
blocking so the player has been able to step on them to trigger interaction).
Interactive tiles (signs, towns, castles, chests, dwellings, foes,
artifacts, telecaves, navmaps, orbs) have not been in the .dat file:
stamp_objects and stamp_placements have applied them from the zone's
JSON arrays + the salt-time placements at zone load. The Glory of Rome's .dat
files have been built from sources in art/maps/ by tools/romeart.py map build.
11. Random generation
- World catalog: a pack has held 256 worlds, selected by an 8-bit index
(
--seed 0–255). Without--seedthe index has been derived from system time + name + class. The index (not the expanded seed) has been what the save has stored, so a world has reloaded exactly. - Seed expansion:
GameSeedFromIndex(engine/game.c) has avalanched the index into a full-widthuint64_tbefore anything reads it. This has been load-bearing, not cosmetic: the engine has consumed the seed at three widths,(seed >> 8)for spawn rolls (engine/flows.c),(unsigned)truncation for chest/dwelling/weekly-salt hashes, and all 64 bits in the world LCG, so a raw0–255seed would hand the first of those a constant 0 and leave the rest with 8 bits of entropy. Expanding first has kept all 256 worlds distinct through every path. - RNG: linear-congruential,
game_rng_next(min, max)inengine/game.c. - Salting (artifacts, dwellings, navmaps, orbs, telecaves, friendly foes, villain placement, scepter location, dwelling troop kinds, town spell selection) has all used this RNG, so a given index has produced a reproducible game.
- Catalog identity: a world has been a pure function of (index, the
GameSeedFromIndexconstants, the order ofgame_rng_nextcalls inGameInit, pack contents). Changing any of the last three re-maps every world and invalidates recorded per-world results. - The salt budget per zone has been configurable in
game.jsonunder each zone'ssaltblock.
12. Tools (tools/)
The pack extractor has been C and has compiled into the main openbounty
binary; ./build/debug/openbounty --extract has been the invocation.
The extractor has read a KB.EXE distribution and written the King's Bounty
pack, file for file the same as assets/kings-bounty (§3, --extract). Its
port-authored sections come from tools/extract_gamejson_const.inc,
regenerated with python3 scripts/gen_extract_constants.py after the pack is
edited (tests/unit/test_extract_constants.c fails until it is). It has been
broken into one translation unit per pipeline stage:
extract_unpack.c, extract_lzw.c, extract_vga.c, extract_png.c,
extract_chrome.c, extract_gamejson.c, extract_io.c, plus the dispatcher
extract.c.
Packing a loose asset tree into a .openbounty zip has been done by the
engine binary itself: ./build/debug/openbounty --pack-dir <src> <out_zip>.
The game and its build have used no Python. The rest of tools/ has been
The Glory of Rome authoring tools, which the build has never run: romeart.py,
the one art and map script (romeart.py --help lists every command): the
terrain tiles and each zone's whole set, the maps (build, check, lint, render,
placing and moving objects), screen and combat art, review pages, the art
record (provenance check, run by make check-maps), and Retro Diffusion
generation, which has posted only with --run. It needs the Python packages
pinned in scripts/requirements-art.txt. capture.sh and walkthrough.sh have
driven a running window for screenshots; detcheck.sh has checked
determinism.
13. Where to look first
- Adding a game-state feature:
engine/game.cand its siblings (game_world.cthe new game and the salting,game_army.cthe army,game_foes.cthe foes,game_storage.cthe tables),engine/include/game.h(struct fields), and possibly the relevant flow handler insrc/shell_*.c. State changes have belonged in the engine; UI flow has belonged in the shell. - Fixing a dialog text bug: the text has lived in the pack's
strings/en.json, loaded viaengine/resources.c; the engine has carried no text of its own. - Debugging movement:
GameStep(engine/step.c) has been the entry point;engine/adventure.c adventure_walkable_*has controlled what's passable. - Debugging an interact:
engine/adventure.c adventure_handle_interacthas classified the tile;engine/step.chas read the result and dispatched to flows / screen openers (engine/flows.c, the host callbacks inengine/include/ui_host.h). - Save format change: the writer in
engine/state_serialize.c, the reader inengine/savegame.c. A new key the reader can fall back on when it is absent has keptSAVE_VERSION; any other change has bumped it inengine/include/savegame.hand updated the goldentests/fixtures/save_v1.dat(docs/OPENBOUNTY-SPEC.mdREQ-414). - Adding a CLI flag: list it in
cli_flags[], give it aCliOptionsfield and parse it incli_parse(all insrc/cli_flags.{c,h}); for an early-exit mode usesrc/shell_earlyexit.c. Add its row to §3 and its name todocs/OPENBOUNTY-SPEC.mdREQ-480:tests/unit/test_cli_flags.chas failed until all three agree. - Adding a combat ability/spell: math in
engine/combat.c, player input wiring insrc/combat_loop.c. Add a golden digest intests/regression/test_combat_digests.cif it changes the formula. - Adding an adventure spell: implementation in
engine/spells_adventure.c, dispatch indispatch_adventure_spell. Modal continuations have routed throughengine/include/pending.h.
Comentarios (0)
Inicia sesión para dejar un comentario.
Sé el primero en comentar sobre este juego.