PORTCLUB
Cabecera de King's Bounty (OpenBounty)
Port nativo

King's Bounty (OpenBounty)

Port nativo de King's Bounty (OpenBounty) para PC.

King's Bounty (OpenBounty)

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:

https://github.com/dannyheskett/openbounty/releases

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 with assets/kings-bounty.openbounty beside it; it has started with no flags. ./openbounty --extract has also built the same pack from a copy of the DOS distribution (§3).
  • The Glory of Rome (gloryofrome-build-N-*): the same binary with assets/glory-of-rome.openbounty beside 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/) and gloryofrome-build-N-web-wasm.zip (The Glory of Rome, danheskett.com/dist/gloryofrome/).
  • iOS (.ipa, also on TestFlight) and Android (sideload .apk and 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-gcc and i686-w64-mingw32-gcc toolchains
  • raylib built once: ./scripts/build_raylib_windows.sh (installs both third_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, then emsdk install latest && emsdk activate latest and source third_party/emsdk/emsdk_env.sh. CI has used setup-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).

FlagArgumentEffect
--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).
--seedNHas 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.
--windowWxHHas 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:

LayerWhat 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.

FlagArgumentEffect
-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).
-sSUITEOnly run suites whose name contains the substring.
-tTESTOnly run tests whose name contains the substring.
-e-Require exact-name match for -s / -t instead of substring.
-xEXCLUDEExclude 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

TargetWhat it has done
make / make allBuilt build/debug/openbounty + libobengine.a + pack zips + library boundary check + iOS purity check + the touch and page guards.
make testRun build/openbounty-test via greatest: the unit, regression, e2e and autoplay layers, including the combat-formula golden digests.
make releasebuild/release/openbounty, -O2 -DNDEBUG, libgcc linked statically.
make windowsCross-compiled openbounty-x64.exe and openbounty-x86.exe.
make windows-debugThe same with a console attached for stderr.
make macopenbounty-mac (universal binary, on macOS).
make webWebAssembly build, one per pack -> build/web/<pack>/openbounty.{html,js,wasm,data} (needs emsdk).
make web-serveBuilt both web builds + served them on http://localhost:8080/<pack>/openbounty.html.
make android / make android-playThe Glory of Rome APK (build/gloryofrome.apk) / the upload-signed AAB (needs the Android NDK + SDK build-tools).
make ios-sim / make iosThe Glory of Rome Simulator .app / device .ipa (on macOS).
make extractWrapper for ./build/debug/openbounty --extract.
make extract-packRegenerated 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-iosThe two web zips and the mobile packages in dist/.
make cleanRemoved 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_SCALE in src/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_VERSION in engine/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_COUNT in engine/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/openbounty if set, else ~/.local/share/openbounty.
    • Windows: %APPDATA%\OpenBounty.
    • macOS: ~/Library/Application Support/OpenBounty.
    • Web: /saves, an IndexedDB-backed mount the page has synced.
  • --save-dir <dir>, iOS (Documents/saves) and Android (the app's private saves directory) 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 = 16
  • GAME_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)

IDNameStarting goldStarting army
knightKnight750020 Militia + 2 Archers
paladinPaladin1000020 Peasants + 20 Militia
sorceressSorceress1000030 Peasants + 10 Sprites
barbarianBarbarian750020 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

IDDaysScore multiplier
Easy900×0.5
Normal600×1
Hard400×2
Impossible200×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_army sprite.
  • 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_continent placed on random chest slots.
  • 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:

  1. Evaluated all 9 cells of the foe's 3×3 neighborhood.
  2. Scored each cell by Euclidean distance to the hero's previous position.
  3. Given non-center cells that are unwalkable (or another interactive) a sentinel max-distance score, so they have never been picked.
  4. Not excluded the hero's current tile: a foe landing on the hero has been the combat trigger.
  5. Moved the foe to the lowest-score cell.
  6. 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:

  1. Registered every wandering_armies[] entry as a hostile foe with a rolled garrison (using the per-continent tier spawn table).
  2. Built a "barrel" of all chest slots in that zone.
  3. Tagged random barrel slots with the salt budget kinds: artifacts, navmaps, orbs, telecaves, dwellings, friendly foes.
  4. Created a SaltedPlacement (or FoeState for friendlies) for each tagged slot. Friendly foes have used the same g->foes[] table as hostiles.
  5. Left MapLoadZoneWithPlacements to stamp these onto the loaded map.

Day/week tick (GameOnStep → end_day)

On each step the engine has:

  1. Decremented time_stop and skipped the day tick, if time_stop > 0.
  2. Zeroed steps_left_today on desert terrain, else decremented it.
  3. Run end_day while steps_left_today <= 0, which may fire week rollover.
  4. 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:

CombatAdventure
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 × 10 steps to time_stop. Steps during time_stop have 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)

IDEffect
Sword of Prowess+50% army damage
Shield of Protection-25% army damage taken
Crown of CommandDoubles leadership
Articles of NobilityIncreases weekly commission
Amulet of AugmentationDoubles spell power
Ring of HeroismDoubles max spells carried
Book of NecrosNo effect (the original's unimplemented slot, REQ-333)
Anchor of AdmirabilityBoat 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_left reaches 0. It has triggered show_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 the Combat struct, combat_init, combat_ai_action, combat_run_headless, combat_test_digest, the spell_* 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

KeyAction
↑ / Numpad 8Move N
↓ / Numpad 2Move S
← / Numpad 4Move W
→ / Numpad 6Move E
Numpad 7 / HomeMove NW
Numpad 9 / PgUpMove NE
Numpad 1 / EndMove SW
Numpad 3 / PgDnMove SE
Numpad 5 (and 5 in modern)Rest one day in place

Views (toggle on/off)

KeyView
AArmy (per-troop stats)
VCharacter (sheet)
IContract
PPuzzle (5×5 reveal grid)
MWorldmap minimap
CControls (settings)
OOptions (legacy); the game menu (modern)

Actions

KeyAction
SSearch current tile (10 days)
UUse magic (cast adventure spell)
WWait until end of week
FFly (mount)
LLand (dismount)
DDismiss army
NNew continent (sail)
GGoto (modern): pick a seen tile on the world map, and travel there

Meta

KeyAction
QLegacy: 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+QQuit without saving, after a yes/no (on an open message too; on the legacy Q save message, at once)
Alt+EnterToggle fullscreen
Backtick (`)Manual screenshot to screenshots/shot_NNNN.png
EscDismiss view / cancel prompt; in modern mode, open the game menu

Gamepad

On the map; a keyboard action in the same frame has won.

ButtonAction
D-pad / left stickMove
ASearch
XUse magic
YWait until end of week
LBArmy
RBCharacter
LTFly
RTLand
StartWorldmap
BackOptions (legacy); the game menu (modern)
BCancel, 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

KeyAction
Arrows / numpad / Home, End, PgUp, PgDnMove the active unit (8 directions)
Space / W / Numpad 5Wait (skip the unit's turn)
SShoot: pick an enemy
FFly: pick an empty cell (flyers only)
UUse magic: a lettered spell menu, A–G (legacy); the combat menu's spells page (modern)
GGive up (yes/no)
AArmy view
VCharacter view
CControls
OOptions (legacy)
Enter / Space / A / CConfirm the picked cell (shoot, fly, spell target)
EscCancel a pick; close a view; in modern mode, open the combat menu or go back a page

Dialog dismiss

KeyAction
Any non-modifier keyAdvance / dismiss a message
Ctrl+QClose the message and ask to quit without saving (on the legacy Q save message, quit at once)
Y / NYes / No on yes-no prompts
1–5 (or numpad)Choice on numbered prompts
A / BChoice on two-choice prompts
Digits / BackspaceType a count (legacy)
EnterYes on a yes-no prompt and commit a count (legacy); act on the cursor's row (modern)
SpaceAct on the cursor's row, commit a count (modern)
ArrowsMove the cursor; step a count by 1 (←/→) or 10 (↓/↑) (modern)
EscCancel / 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 for Press 'ESC' to exit while 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 the pending_flow state machine in src/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_codes in game.json (0x20..0x7E ASCII chars have corresponded to tile codes).
  • Width and height have come from each zone's width/height JSON 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 --seed the 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-width uint64_t before 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 raw 0–255 seed 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) in engine/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 GameSeedFromIndex constants, the order of game_rng_next calls in GameInit, 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.json under each zone's salt block.

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.c and its siblings (game_world.c the new game and the salting, game_army.c the army, game_foes.c the foes, game_storage.c the tables), engine/include/game.h (struct fields), and possibly the relevant flow handler in src/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 via engine/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_interact has classified the tile; engine/step.c has read the result and dispatched to flows / screen openers (engine/flows.c, the host callbacks in engine/include/ui_host.h).
  • Save format change: the writer in engine/state_serialize.c, the reader in engine/savegame.c. A new key the reader can fall back on when it is absent has kept SAVE_VERSION; any other change has bumped it in engine/include/savegame.h and updated the golden tests/fixtures/save_v1.dat (docs/OPENBOUNTY-SPEC.md REQ-414).
  • Adding a CLI flag: list it in cli_flags[], give it a CliOptions field and parse it in cli_parse (all in src/cli_flags.{c,h}); for an early-exit mode use src/shell_earlyexit.c. Add its row to §3 and its name to docs/OPENBOUNTY-SPEC.md REQ-480: tests/unit/test_cli_flags.c has failed until all three agree.
  • Adding a combat ability/spell: math in engine/combat.c, player input wiring in src/combat_loop.c. Add a golden digest in tests/regression/test_combat_digests.c if it changes the formula.
  • Adding an adventure spell: implementation in engine/spells_adventure.c, dispatch in dispatch_adventure_spell. Modal continuations have routed through engine/include/pending.h.

Comentarios (0)

Inicia sesión para dejar un comentario.

Sé el primero en comentar sobre este juego.