PINONITE/NIKOLAS PINON ← CREATIVE SYSTEMS
PINONITE · FIELD NOTES · CREATIVE SYSTEMS
PUBLISHEDAugust 11, 2026

PERSONAL PROJECT · BUILT AFTER HOURS

Creative coding CREATIVE SYSTEMS ↗

GOAP game AI: nobody told the blacksmith to wait

A Goal-Oriented Action Planning (GOAP) game AI simulation where autonomous characters search for plans, share resources, and replan when the town changes.

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.

SESSION 01 / 05

Watch one search assemble itself

SAME RUN / LIVE
Nothing to click here — watch one 14-second cycle. The dashed connector always points backwards, from an unmet need to the effect that produces it, while the finished plan then lights up forwards, left to right.

A 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.

SESSION 02 / 05

Read one character's plan while it runs

SAME RUN / LIVE
Click the cook — in the isometric slab or as a dot on the map — and read the numbered plan in the inspector. Completed steps grey out with a tick. Push the speed to 4× and watch the cook chop wood again every cycle, because cookMeal set fireLit back to zero.

Waiting 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.

SESSION 03 / 05

Watch a dependency form between two strangers

SAME RUN / LIVE
Click the blacksmith first, before any ingot exists — the phase reads waiting, the plan is empty, and an amber note explains why. Then click the smelter and keep watching: the moment the world ingot count reaches 1, the blacksmith replans on his own.

Six 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.

SESSION 04 / 05

Let six plans interfere with each other

SAME RUN / LIVE
Click along the chain — miner, smelter, blacksmith, farmer, carpenter, baker — and read the two bars under Wants: a quota that grows as the day runs out, a hunger that grows on its own. Early on, everyone downstream of the blacksmith is waiting on tools, and hunger is what they do about the wait. Stay until the day counter rolls: every stockpile zeroes, every want resets, and the town re-derives all of it.

Wanting 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.

SESSION 05 / 05

Watch a want get out-shouted

SAME RUN / LIVE
Click the baker and read her two bars: Keep the shelf stocked sits at zero while four loaves are up, then climbs the moment the smiths walk over and eat them back down. Click a smith mid-quota and wait for the switch line to appear. Nobody rings a dinner bell here — the shelf empties, a number crosses another number by more than 0.15, and the plan changes.

The 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.