A deterministic, data-driven game engine for text adventure and RPG games. Go engine, Lua content, JSON saves.
Think King's Quest meets a modern rules engine — all game behavior is defined in Lua data files, compiled once at startup into Go structs. Zero Lua execution during gameplay. Same state + same command = identical result, always.
go build -o questcore ./cmd/questcore
./questcore games/lost_crown/Requires Go 1.21+.
Type commands in natural English. The parser understands 90+ verb synonyms, multi-word names, and articles.
go north walk east run south
n / s / e / w ne / nw / se / sw
up / down u / d
look examine fireplace take the rusty key
read old book use key on door open chest
drop sword give coin to merchant
talk to captain speak with scholar
ask elara about crown ask captain about passage
inventory (i) wait (z) again (g)
/help /save [name] /load [name]
/quit
Games are directories of Lua files. QuestCore loads them at startup and compiles them into Go structs — Lua is a data language here, not a scripting runtime.
Room "great_hall" {
description = "A grand hall with a massive fireplace.",
exits = { north = "throne_room", east = "library" },
fallbacks = { take = "Everything here belongs to the king." }
}Item "rusty_key" {
name = "rusty key",
description = "A small iron key, rough with rust.",
location = "castle_gates"
}NPC "scholar" {
name = "Scholar Elara",
description = "An elderly woman in ink-stained robes.",
location = "library",
topics = {
greet = {
text = "'The answer lies in the books, as it always does.'",
effects = { SetFlag("met_scholar", true) }
},
passage = {
text = "'Push the third stone from the left.'",
requires = { HasItem("old_book"), FlagSet("met_scholar") },
effects = { SetFlag("knows_passage", true) }
}
}
}Rules map intents to effects. First match wins.
Rule("push_wall_library",
When { verb = "push", object = "wall" },
{ InRoom("library"), FlagSet("knows_passage") },
Then {
Say("The wall slides away, revealing a dark passage!"),
OpenExit("library", "north", "secret_passage"),
SetFlag("passage_open", true)
}
)Say, GiveItem, RemoveItem, SetFlag, IncCounter, SetCounter, SetProp, MoveEntity, MovePlayer, OpenExit, CloseExit, EmitEvent, Stop
HasItem, FlagSet, FlagNot, FlagIs, InRoom, PropIs, CounterGt, CounterLt, Not
cmd/questcore/ Entry point
cli/ Terminal I/O, meta-commands (/save, /load, /help)
engine/
parser/ Command string → Intent (verb/object/target)
resolve/ Entity name → entity ID (room-scoped, partial matching)
rules/ Rules pipeline: collect → filter → rank → select → effects
effects/ ApplyEffects: the single point of state mutation
events/ Event emission and handler dispatch
dialogue/ NPC topic system
state/ State struct, property lookups, entity helpers
save/ JSON serialization
engine.go Step() orchestrator wiring it all together
types/ Shared data types (no logic)
loader/ Lua VM, sandbox, compile, validate
games/ Example game content
Seven invariants the engine never violates:
- Lua is compile-time only. VM runs once at load, then discarded.
- Rules engine is a pure function.
(state, intent) → effects. - Effects are the instruction set. Small, fixed, atomic operations.
- First match wins. Room → target entity → object entity → global → fallback.
- State is flat. Flags are bools, counters are ints, no deep nesting.
- Engine knows nothing about game content. All behavior comes from Lua.
- Determinism. Same state + same command + same RNG seed = identical result.
MIT — see LICENSE.