@balpal4495/world-engine
v1.0.0
Published
Portable simulation runtime - world state, event ledger, chronicle, and narrative context
Downloads
206
Readme
World Engine
Portable simulation runtime for building worlds with memory.
World Engine tracks entities, relationships, and events — then derives significance, causal chains, themes, and historical reports from them. Games are the primary use case, but it works anywhere you need a world that remembers what happened.
npm install @balpal4495/world-engineThe problem
Most simulations forget.
Event → Outcome → ForgottenWorld Engine turns that into:
Event → Consequence → Historical Significance → LegacyEvery meaningful action becomes part of history.
Quick start
import { WorldEngine } from "@balpal4495/world-engine"
const world = new WorldEngine()
// Create entities
const hero = world.createEntity({ type: "hero", name: "Night Falcon" })
const villain = world.createEntity({ type: "villain", name: "Shadow Lord" })
// Create a relationship
world.createRelationship({ source: hero.id, target: villain.id, type: "rivals" })
// Advance time
world.tick()
// Record what happened
const event = world.recordEvent({
actor: hero.id,
action: "save_city",
target: villain.id,
outcome: "Shadow Lord driven back",
})
world.tick()
// Ask the chronicle what mattered
const chronicle = world.chronicle()
console.log(chronicle.getSignificantEvents())
console.log(chronicle.getThemes())
console.log(chronicle.generateReport())Core concepts
Entities
Everything in a world is an entity — hero, villain, kingdom, city, army, player.
const city = world.createEntity({
type: "city",
name: "Ironhold",
attributes: { population: 12000 },
})
world.getEntity(city.id)
world.destroyEntity(city.id)Relationships
Entities connect to each other. Connections create context for events.
world.createRelationship({ source: hero.id, target: city.id, type: "protects" })
world.getRelationships(hero.id)
world.endRelationship(relationshipId)Events
Events are the source of truth. They are immutable once recorded.
world.recordEvent({
actor: hero.id,
action: "defend",
target: city.id,
outcome: "Attack repelled",
causedBy: previousEventId, // optional causal link
metadata: { damage: 40 }, // optional arbitrary data
})Ticks
Time moves forward one tick at a time. Every entity, event, and relationship is stamped with the tick it occurred on.
world.tick() // advance by one tick
world.currentTick // read the current tickChronicle
Chronicle analyses the event ledger and answers: what mattered, why, and what caused what?
const chronicle = world.chronicle()
// Events ranked by significance
chronicle.getSignificantEvents()
// Full causal chain from any event
chronicle.getEventChain(eventId)
// Recurring themes across history
chronicle.getThemes()
// Complete historical report
chronicle.generateReport()No AI required. All analysis is deterministic.
Rules
Rules enforce world-specific constraints before any action is committed.
const world = new WorldEngine()
world.addRule({
name: "no-self-target",
onEvent(proposal) {
if (proposal.target === proposal.actor) {
return { rule: "no-self-target", reason: "An entity cannot target itself." }
}
},
})
world.addRule({
name: "no-hero-villain-alliance",
onCreateRelationship(proposal, ctx) {
const source = ctx.getEntity(proposal.source)
const target = ctx.getEntity(proposal.target)
if (source?.type === "hero" && target?.type === "villain" && proposal.type === "ally") {
return { rule: "no-hero-villain-alliance", reason: "Heroes cannot ally villains." }
}
},
})Violations throw a RuleViolationError with the full list of violations.
import { RuleViolationError } from "@balpal4495/world-engine"
try {
world.recordEvent({ actor: hero.id, action: "fly", target: hero.id })
} catch (e) {
if (e instanceof RuleViolationError) {
console.log(e.violations) // [{ rule, reason }, ...]
}
}Rules are pure functions — they never modify state, only observe and block.
Archive
For long-running worlds, World Engine automatically compresses old history into archived eras, keeping the live ledger lean.
const world = new WorldEngine({
eraLength: 100, // compress every 100 ticks (default)
liveWindowSize: 50, // keep the last 50 ticks in full detail (default)
})Retiring entities
When a character's story is complete, retire them. Their history is compressed into a legacy; the raw events are removed from the live ledger.
const legacy = world.retireEntity(hero.id)
// legacy.entity, legacy.significantEvents, legacy.dominantActions, legacy.summary
world.chronicle().getEntityLegacy(hero.id)
world.chronicle().getEntityLegacies()
world.chronicle().getArchivedEras()Narrative
Narrative turns history into prose. AI is optional — there is always a deterministic template fallback.
const narrative = world.narrative()
// Template-based (no AI)
await narrative.generateReport(chronicle.generateReport())
await narrative.describeEvent(event)
await narrative.describeChain(chain)
await narrative.describeThemes(themes)
// AI-enriched
const narrative = world.narrative({
llm: async (messages) => {
// call your LLM provider here
return await myLLM(messages)
},
})The simulation is fully functional if the LLM disappears. Only prose quality is affected.
Persistence
Save and load worlds with the built-in SQLite adapter (Node.js / Electron).
import { WorldEngine } from "@balpal4495/world-engine"
import { SqliteAdapter } from "@balpal4495/world-engine/sqlite"
const adapter = new SqliteAdapter("./saves/game.db")
// or: new SqliteAdapter(":memory:") for tests
// Save
adapter.save("slot-1", world.save())
// Load
const snap = adapter.load("slot-1")
if (snap) world.load(snap)
// List saves
adapter.list() // [{ id, tick, savedAt }, ...]
// Delete
adapter.delete("slot-1")
adapter.close()The PersistenceAdapter interface is platform-agnostic — implement it for IndexedDB, AsyncStorage, or any other backend.
import type { PersistenceAdapter } from "@balpal4495/world-engine"
class MyAdapter implements PersistenceAdapter {
save(id, snapshot) { /* ... */ }
load(id) { /* ... */ }
list() { /* ... */ }
delete(id) { /* ... */ }
}Save / load
Snapshots are plain JSON — safe to serialise, store, and transfer.
const snapshot = world.save()
// { version: 1, tick, entities, relationships, events, archivedEras, entityLegacies }
const json = JSON.stringify(snapshot)
const world2 = new WorldEngine()
world2.load(JSON.parse(json))Design rules
- No platform code in core. Runs identically in browser, Node, Electron, React Native.
- No AI required. Significance, themes, causal chains — all deterministic.
- AI never changes history. The Narrative layer only describes what the simulation recorded.
- Events are immutable. Nothing in the engine modifies a recorded event.
- Rules are pure. They observe state, never change it.
Development
npm test # run all tests (vitest)
npm run typecheck # TypeScript check
npm run build # build ESM + CJS + types to dist/Platform support
| Platform | Core engine | SQLite adapter | |-----------------|-------------|----------------| | Node.js | ✓ | ✓ | | Electron | ✓ | ✓ | | Browser | ✓ | — | | React Native | ✓ | — | | Bun / Deno | ✓ | ✓ (via compat) |
