Portable C17 core engine for Carmen Sandiego-style world exploration games, starting with an Islamicate world and extensible to other cultural realms — zero external deps, clean C ABI, FFI- and WebAssembly-ready.
libcarmen is a hobby project: a small, portable, dependency-light core engine designed to be cleanly wrapped by other languages and frameworks. The C17 core owns all game logic and state; front-ends and language bindings simply drive it through a stable, UI-agnostic API.
The public interface is intentionally shaped for wrapping:
- Plain-data structs and a C ABI -- easy to bind from Ruby (FFI), Python (ctypes/cffi), Rust, Go (cgo), Node (N-API), or compile to WebAssembly via Emscripten.
- Clear separation of queries and actions -- read-only query functions for rendering the UI, and discrete action functions (
travel,investigate,issue_warrant,arrest) for input. - Built-in i18n -- user-facing strings are looked up via locale files, so wrappers and UIs stay language-neutral.
- No embedded UI or I/O in the core -- the terminal demo is just one front-end; the same library is meant to back native, web, and TUI clients equally.
Ruby and Python wrappers (and other language bindings and front-ends) are intended to build on top of this core rather than reimplement it.
The built-in world covers 22 important Muslim cities globally, aiming for a tasteful balance of the historical and the modern — from classical centers of learning and trade to present-day capitals and cultural hubs. They span the breadth of the Islamicate world: Istanbul (Süleymaniye Mosque, Topkapı Palace), Fez (Al-Qarawiyyin, the ancient medina), Samarkand (the Registan, Bibi-Khanum Mosque), Zanzibar (Stone Town, Hamamni Persian Baths), Lahore (Badshahi Mosque, Lahore Fort), Kuala Lumpur (Petronas Towers), and sixteen others — each with up to four investigable sites across types: market, mosque, museum, landmark, and park. The selection is a starting point, not a canon; the preset system (presets/islamic.jsonc) is designed for adding worlds rooted in other cultural geographies.
The criminal organization behind each case is FITNA — a roster of 16 thieves defined in include/carmen/villain.h with names drawn from Arabic, Persian, and Urdu traditions: Qamar Samarkandi ("The Moon"), Layla Lapis ("Midnight Blue"), Tariq al-Tariq ("The Morning Star"), Soraya Samum ("The Sandstorm"), Rumi the Riddle ("The Poet"), and eleven others. Each villain carries four identity clues the player collects at the hideout in order to issue a warrant.
The static world (built once, read-only during play):
GameWorld -- stb_ds string hash map (city id -> slot) with secondary index arrays
City -- struct: id, name, local_name, country, continent, lat/lon, sites[], connections[], inbound_clues[]
Site -- struct: id, name, site_type (clues are drawn at case-gen time from the city's inbound_clues[] pool)
Connection -- struct: destination_id, distance_km, transport_modeThe scenario and play state (generated per game from the world plus settings):
CaseSettings -- difficulty + optional overrides (trail length, time budget, move limit, ...)
Case -- one generated scenario, built from the world + settings
Villain -- culprit picked from the FITNA_VILLAINS[] catalog (id clues, alias, gender)
Artifact -- stolen item from the CARMEN_ARTIFACTS[] catalog (origin city = crime scene)
trail[] -- ordered chain of connected cities from origin to hideout
stops[] -- per-city active sites and their assigned clues
Session -- live play state over a Case
current city, visited history, time/moves remaining
notebook[] -- clues gathered by investigating sites
evidence[] -- villain identity clues collected at the hideout
warrant -- issued villain index (gates a valid arrest)
status -- PLAYING / WON / LOST_* outcomeThe core exposes read-only queries for rendering (current city, connections, active sites, notebook, evidence, time/moves) and discrete actions for input (travel, investigate, issue_warrant, arrest).
- stb_ds (header-only, MIT/public domain) for O(1) city lookup by string ID via
shput/shgetistring hash map macros - All City structs live in a contiguous backing array (
GameWorld.storage[]) -- the stb_ds hash map stores{key, slot_index}entries, not the cities themselves - Fixed-capacity arrays with
MAX_*constants for sites, connections, clues - Secondary indices for continent/country use simple linear arrays (6 continents, ~20 countries -- scan is effectively O(1))
- BFS uses stack-allocated queues and visited arrays -- no dynamic memory
- City struct is plain data -- no hash-table bookkeeping embedded in it
<stdbool.h>forbool/true/false- Designated compound literals:
(BFSEntry){slot, depth} - Mixed declarations and code (C99+)
-std=c17 -Wall -Wextra -pedanticclean
All third-party code is vendored under vendor/ -- there are no external dependencies to install.
- stb_ds.h (
vendor/stb/, MIT / public domain) -- string hash map / dynamic arrays - cJSON (
vendor/cjson/, MIT) -- parses the locale JSON files - toml-c (
vendor/toml-c/, MIT) -- parses optional case settings files - utf8.h (
vendor/utf8/, public domain) -- UTF-8 helpers - Unity (
vendor/unity/, MIT) -- unit test framework (test builds only) - Any C17-compliant compiler (GCC 8+, Clang 7+, MSVC 2019+) -- Linux and macOS are built and tested with both Make and CMake in CI; Windows/MSVC and WebAssembly (emscripten) are built and tested with CMake in CI (see
.github/workflows/ci.yml)
make # static library + demo binary (build/trail_demo)
make lib # static + shared library
make test # run all unit tests
make test-sanitize # run all unit tests under ASan + UBSan (GCC/Clang only)
make coverage # test coverage report (requires lcov)
make package # self-contained macOS demo tarball (libcarmen-demo-<version>-macos-<arch>.tar.gz)Or with CMake (also how the WebAssembly and Windows/MSVC builds are configured -- see Building below and .github/workflows/ci.yml):
cmake -S . -B build && cmake --build build -j && ctest --test-dir build
# ASan + UBSan (GCC/Clang only; ignored on MSVC and when cross-compiling)
cmake -S . -B build-sanitize -DCARMEN_SANITIZE=address,undefined
cmake --build build-sanitize -j && ctest --test-dir build-sanitizeOr manually:
# 1. Build the world generator and compile the preset into C. The built-in
# world lives in presets/islamic.jsonc; gen_world validates it and emits C.
cc -std=c17 -O2 -Iinclude -Ivendor/cjson \
tools/gen_world.c vendor/cjson/cJSON.c -o gen_world
./gen_world presets/islamic.jsonc world_islamic_generated.c
# 2. Build the demo (note -Isrc so the generated file finds seed_helpers.h).
cc -std=c17 -Wall -Wextra -pedantic -O2 \
-Iinclude -Isrc -Ivendor/stb -Ivendor/utf8 -Ivendor/cjson -Ivendor/toml-c \
src/utf8.c src/site.c src/connection.c src/city.c src/game_world.c \
world_islamic_generated.c src/villain.c src/artifact.c src/case.c \
src/session.c src/settings.c src/i18n.c vendor/cjson/cJSON.c \
examples/trail_demo.c -o trail_demoRun from the repository root -- the demo loads its locale file via the relative path locales/<locale>.json:
./build/trail_demo # default locale "en"
./build/trail_demo es # use locales/es.json
./build/trail_demo en case.toml # optional case settings filemake install # installs to /usr/local
make install PREFIX=/opt # custom prefix
make uninstallAfter installing, link with -lcarmen or use pkg-config:
cc $(pkg-config --cflags --libs libcarmen) my_game.c -o my_gameConsumer code includes the library with the carmen/ prefix:
#include <carmen/carmen.h>Makefile Build rules (static/shared lib, tests, coverage, install)
LICENSE MIT license
carmen.pc.in pkg-config template
include/carmen/
carmen.h Umbrella header (includes everything below)
carmen_version.h Version macros (CARMEN_VERSION_MAJOR/MINOR/PATCH)
carmen_export.h Symbol visibility / DLL export macros
utf8.h UTF-8 helper API
clue.h Clue struct
site.h Site struct and API
connection.h Connection struct and API
city.h City composite struct and API
game_world.h GameWorld hash map + indices + BFS API
world_islamic.h Built-in world builder API (impl generated at build time)
villain.h Villain roster and API
artifact.h Stolen artifact struct and API
case.h Case (villain + trail + settings) API
session.h Play session state and actions
settings.h Case settings (difficulty, TOML loading)
save.h Session JSON save/load (serialize/restore)
i18n.h Locale loading and string lookup
src/
utf8.c UTF-8 helpers
site.c Site implementation
connection.c Connection implementation
city.c City implementation
game_world.c Hash map, secondary indices, BFS, shortest path
seed_helpers.h Inline helpers used by the generated world builder
villain.c Villain roster
artifact.c Stolen artifact implementation
case.c Case generation (villain, trail, briefing)
session.c Session state and actions (travel/investigate/warrant/arrest)
settings.c Case settings + TOML loading
save.c Session JSON serialization (save/load, cJSON)
i18n.c Locale JSON loading and string lookup
presets/
islamic.jsonc Built-in world data (single source of truth, JSONC)
tools/
gen_world.c Host tool: validates a preset and emits it as C
CMakeLists.txt Standalone build for the host tool (used when cross-compiling)
examples/
trail_demo.c main() demo driver (interactive terminal game)
locales/
en.json English locale strings
test/
test_site.c Site unit tests
test_connection.c Connection unit tests
test_city.c City unit tests
test_game_world.c GameWorld unit tests
test_carmen_scenarios.c Full-world integration tests
test_artifact.c Artifact unit tests
test_case.c Case unit tests
test_session.c Session unit tests
test_save.c Session save/load (JSON) tests
test_settings.c Settings unit tests
test_villain.c Villain unit tests
test_world_islamic.c Built-in world golden-value tests
test_i18n.c i18n loading (file + buffer) tests
fixtures/ Invalid presets the generator must reject
vendor/
stb/stb_ds.h Hash map / dynamic array library (vendored)
unity/ Unity test framework (vendored)MIT -- see LICENSE.