Neurosymbolic, by construction.
There are two ways to put AI characters into a game. One is to ask an LLM to generate dialogue and decisions in real time, every frame, for every entity. That is expensive, slow, and the world drifts away from itself after a few minutes. Story Garden takes the other way.
A small symbolic engine carries every entity's behavior tick by tick. It is cheap, deterministic, and costs no AI tokens. The AI joins in at a higher layer, writing new behaviors and decision rules in MiniScript when its imagination is needed. What the AI writes runs.
An entity's behavior is a set of actions, each paired with an expression over named considerations, combined with fuzzy and, or, not, nested arbitrarily deep. A consideration is anything that returns a fuzzy number between 0 and 1. Every tick, every entity scores its tree, and the highest branch runs.
A village NPC's whole mind:
{
"INITIATE_DIALOGUE": "(player_in_interaction_range)",
"SHARE_MEMORY": "(player_distance_close
and (memory_sharing_opportunity or knows_song_fragment)
and not already_shared_with_player)",
"RECONSTRUCT_STORY": "(player_distance_close
and all_fragments_shared
and not story_already_reconstructed)",
"OFFER_COMFORT": "(player_distance_close
and player_seems_disheartened
and not recently_consoled)",
"WANDER_LOCAL": "(point_1)"
}
Some considerations are MiniScript functions, computing on position, memory, time, what's in someone's hand. memory_sharing_opportunity is one of them:
// memory_sharing_opportunity, fuzzy predicate, returns 0..1
memory_sharing_opportunity = function(args)
selfPos = args["entity"]["pos"]
targetPos = args["target"]["pos"]
dx = selfPos.x - targetPos.x
dy = selfPos.y - targetPos.y
dz = selfPos.z - targetPos.z
if sqrt(dx*dx + dy*dy + dz*dz) > 10 then return 0
social = args["entityData"]["social_memory"]
last = social["last_interaction_with"][args["target"]["uid"]]
if WorldTime() - last < 30 then return 0
return 1
end function
Other considerations are concepts the AI is asked to score directly: "does this player seem disheartened?", "is this build inspired enough to react to?", "is this offering meaningful for the ritual?". Anything the AI can assess and return as a number. No MiniScript at the leaf, just a value between 0 and 1, dropped back in for the next tick. The neurosymbolic seam doesn't sit at one boundary; it runs through every leaf.
The AI also authors at the higher layer. It composes new behaviors from the existing palette, and writes new actions and considerations as MiniScript when it needs them. Here is what the spell-weaver returned the day a player typed !cast_spell "a small fox that knows where it came from":
// spell-weaver output, hot-loaded into the live simulation
{
"RETURN_HOME": "(at_dusk and not at_home_location)",
"APPROACH_FAMILIAR": "(player_distance_close and reminded_of_origin)",
"LINGER_AT_MEMORY": "(near_planted_memory)",
"WANDER_LOCAL": "(point_1)"
}
// near_planted_memory, a new consideration, written by the model
near_planted_memory = function(args)
pos = args["entity"]["pos"]
planted = args["globalData"]["planted_memories"]
for m in planted
dx = pos.x - m["pos"].x
dy = pos.y - m["pos"].y
dz = pos.z - m["pos"].z
if sqrt(dx*dx + dy*dy + dz*dz) < 5 then return 1
end for
return 0
end function
Old palette, new tree, one new consideration. From the fox's first tick it scores against the same loop as every other villager. It runs every tick. It costs nothing afterward. It persists when the chat session is over.