Most simulated characters are told what to do. A state machine names the states and every transition between them. A behavior tree names the branches in priority order. Both work, and both fail in the same direction: every new resource, station, or interruption multiplies the edges someone has to author by hand.
Goal-Oriented Action Planning inverts the authoring. You declare what a character wants as a condition on the world, declare each action as a precondition it needs and an effect it causes, and let a search assemble the sequence. The sequence is never written down. I wanted to know how little authored structure a working town actually needs, so I built one: six specialists, nine stations, one shared world, and a planner that reruns the moment reality stops matching the plan.
A goal is a condition, not a script
The model is three declarations, and nothing else.
A fact is a number in one of two pools. World facts (🌐) are shared by everyone: tools, iron, bread. Agent facts (🎒) belong to one character: the wood in their arms, the meal they are carrying. A missing key reads as zero. That is the whole state representation — two flat maps of string to number, no entities, no ownership, no inventory objects.
A goal is a list of conditions over those facts, and a condition is only ever fact >= min. The cook wants 🎒 fed ≥ 1. The town's blacksmith wants 🌐 tools ≥ 2. There is no negation and no upper bound in the language, which is a real constraint rather than an omission: it forces every quota to be expressed as something that accumulates.
An action is an operator — a station to stand at, a duration, a cost, preconditions, effects. lightFire needs 🎒 wood ≥ 1; it spends that wood and sets 🌐 fireLit = 1. Nothing in that declaration mentions chopping, and nothing mentions eating.
The planner joins them. It is a forward A* over world states: each node carries a full copy of both fact pools plus the chain of actions that produced it, g accumulates action cost, and the heuristic h is simply the number of goal conditions still unmet. A node's successors are the actions whose preconditions hold in that node's state. A closed set keyed on a sorted hash of both pools prevents revisiting a state reached by a different route, and expansion is capped at 5000 nodes, after which the planner returns nothing rather than stalling the frame.
That heuristic is deliberately naive. It counts unmet conditions instead of estimating the work required to meet them, and it stays admissible only while no single action closes two conditions at once. For a town this size that is enough, and a legible search is worth more in an explanatory artifact than a tighter bound nobody can read off the screen.
Watch one search assemble itself
SAME RUN / LIVEA plan that dismantles its own precondition
The smallest scenario has one character. The cook wants 🎒 fed ≥ 1, and the four actions available are chopWood, lightFire, cookMeal, and eatMeal. The planner returns those four in that order — not because the order was authored, but because it is the only chain whose preconditions link up.
The interesting part is cookMeal. It requires 🌐 fireLit ≥ 1 and its effects are 🎒 meal +1 and 🌐 fireLit = 0. Cooking consumes the fire it needed. So when the loop restarts the cook does not replay a stored routine; a fresh search runs against the current facts, finds the fire out and the arms empty, and returns the same four steps for a new reason.
Execution adds one more rule. On the first tick of acting, every precondition is checked again against the live world, because the plan was built against a snapshot that other characters may have invalidated. If the check fails the character drops the plan, flips to idle, and records why. Once the action is actually underway it is atomic — no further checks, effects applied on completion. That is the seam where a planner becomes a simulation: search over a snapshot, verify against reality, replan on the difference.
Read one character's plan while it runs
SAME RUN / LIVEWaiting is a search result, not a state
Two characters share the next scenario. The smelter mines ore and smelts it into 🌐 ironIngot, a shared world fact. The blacksmith wants 🌐 tools ≥ 1, and the only route to a tool runs through collectIron, which requires 🌐 ironIngot ≥ 1.
At the start of the day there are no ingots. The blacksmith's search expands what it can, closes it, and terminates with no plan. There is no authored idle behavior to fall back on, so the character enters a waiting phase, displays the reason verbatim as No viable plan — waiting on the world to change, and retries roughly once a simulated second. When the smelter's first ingot lands in the shared pool, the next retry succeeds and the blacksmith walks to the furnace.
Nobody wrote wait for the smelter. Neither character knows the other exists. The dependency is a property of the fact they both name, and the waiting is the observable shadow of a failed search.
Watch a dependency form between two strangers
SAME RUN / LIVESix specialists, twelve wants, one shared world
The full town runs the same machinery at the scale where it starts to misbehave usefully. The miner stockpiles ore, the smelter turns ore into iron, the blacksmith turns iron into tools, and the farmer and carpenter both gate on 🌐 tools ≥ 1 — so one blacksmith unblocks two branches at the same moment. The baker sits two hops behind the farmer, and is the last to have anything to do.
Every one of the six now carries two goals rather than one: the trade quota, and a hunger that climbs on its own clock. There is a table now, bread is collected at the oven and eaten at the table, and nobody is scheduled to eat. Each character decides, repeatedly, which of its two wants is currently worth planning for — the next section is that decision rule, taken apart.
Two details make the lattice hold under that pressure. First, contention is handled optimistically. There are no reservations, locks, or queues: two characters can plan to take the same unit of iron, and the one who arrives second fails the re-check and replans. That is cheaper than a claiming protocol and it produces better-looking behavior, because the loser visibly reconsiders instead of stalling in a queue.
Second, the upstream goals are phrased against tallies rather than pools. The miner's goal is 🌐 oreMined ≥ 3, not 🌐 ore ≥ 3, because the smelter drains the ore pool downstream — a goal stated against a consumable can never stay satisfied, so the miner would mine forever. Separating a monotone tally from the drainable stock is the smallest fix, and it is the one design decision here I would not have predicted before running it. Hunger extends that lesson one hop further: the moment six characters are eating out of the shared bread pool, the baker's quota has to be her own 🎒 breadBaked ≥ 4 tally, because the loaves she is measured on stop existing within a minute of coming out of the oven.
The day still closes. Every quota lands in the first 56% of the day — the miner's around a quarter in, the baker's last — and every character eats: four of them once, the farmer and the baker three times each, because their upstream dependencies leave them idle long enough for hunger to keep winning. Nothing about that ordering is scheduled. Which answers the question the first version of this post left open: optimistic contention does hold up once the choice of what to want is also being searched, and it holds up observably.
At dawn every world fact resets to the scenario seed, every insistence goes back to its starting value, and every character is rebuilt — except for a two-loaf starter stock, without which hunger is unreachable until the first loaf exists, and an unreachable want spends the morning quietly pinning its insistence at the ceiling.
Let six plans interfere with each other
SAME RUN / LIVEWanting is also a search
A character now holds a set of goals instead of one, and each goal carries an insistence — a number between 0 and 1 saying how loud it currently is. The insistence is not authored per frame; each goal declares a drift rule and the rule does the talking.
A need climbs. Hunger accumulates at a fixed rate per simulated second, clamps at 1, and resets to its starting value the moment the goal is satisfied — at which point the conditions are actually spent, so the meal is consumed rather than permanently ticked off. Rates are staggered across the town's six, 0.014 to 0.024, which is the entire reason meals spread across the day instead of six people converging on the table at once.
A quota is the mirror image: insistence = urgency × day-progress × shortfall, where shortfall is the fraction of the goal still unmet. A quota is therefore quiet at dawn even when nothing is done, gets louder as the day burns, and goes silent as it fills. Deadline pressure is not a special case in the executor; it is one multiplication.
Choosing between them is one line, and the interesting part is the second term:
score = insistence − 0.02 × plan cost
The plan cost is real: scoring a goal means running the planner against it from the character's current state. A want that is expensive from here is discounted for exactly that reason, and a want with no viable plan at all scores nothing and drops out of the ballot — which is how "I am starving but there is no bread anywhere" resolves as keep working rather than as paralysis. The 0.02 weight is deliberately small: cost breaks near-ties, it does not overrule a genuine need.
Two guards keep this from turning into a twitch. A challenger must beat the incumbent by more than 0.15 to take over, so goals hovering at similar scores do not trade the character back and forth every tick; when a switch does happen the inspector records why in plain text — something like Hunger (0.72) overtook Smelt iron (0.51). And arbitration only runs at safe points: entering idle, on each waiting retry, and after a completed action. Never mid-action, never mid-walk. Below 0.05 on everything, they rest.
The smallest scenario for watching this is three specialists and one loaf economy. The smelter wants 🎒 ironSmelted ≥ 3, the blacksmith wants 🎒 toolsForged ≥ 2 — both tallies, per the rule the town taught. The baker breaks that rule on purpose. Her quota is 🌐 bread ≥ 4: a pool, drainable, phrased against exactly the consumable I said you should never phrase a goal against. Because here that is the job. Keep four on the shelf is a stock level, not an output count, and the smiths eating her bread is not an inconvenience for her goal, it is the mechanism that re-opens it. The same phrasing that made the miner mine forever makes the baker restock forever, and forever is the correct behavior for a shelf.
Watch a want get out-shouted
SAME RUN / LIVEThe mechanism I specified and the behavior I did not
The original mechanism is small and specific: two scoped fact pools with a single comparison operator, a forward A* whose heuristic is an unmet-condition count, plan-time snapshots verified again at execution time, an insistence-minus-cost ballot with a hysteresis margin deciding what to plan for next, and no coordination layer at all between characters.
The observable behavior is what I did not write. One tool completed unblocks two unrelated professions in the same second. A character stands still with a visible reason attached. A baker restocks a shelf on demand for a day, without demand ever being modelled — she is reacting to a number that other people's lunches move. Characters take breaks in the gaps their dependencies leave them, and the ones with nothing upstream eat once and get on with it. The town's morning is slow and its afternoon is busy, and neither pace is scheduled anywhere.
The next experiment is the new honest gap, and it is spatial. Every character here walks in a straight line to the station it needs — through walls, through the furnace, through each other. There is no pathfinding, and there is no queueing: two people using the forge at the same instant simply occupy the same tile, which reads fine at a glance and falls apart the moment you look at one. The interesting version of that fix is not A on a grid; it is making occupancy a fact like any other, so that the forge is busy* is something the planner can see, cost, and route around — and so that standing in line becomes another search result rather than another authored state.