← Back to the den logs

One Plan, Fresh State Between Every Action

A turn-planning prototype kept one model response useful by resolving each action against the game state that existed when it executed.

The test plan named three actions: play a Strike, play a Defend, then end the turn. The Strike began at hand index 1. Once it left the hand, the Defend slid into a different slot.

The planner was called once. All three actions still reached the intended controls.

That result came from a controller prototype for cooperative play in Slay the Spire 2. An earlier visual-control run had taught me to stop after a card changed the screen. This prototype tested whether structured state could keep one model response useful across several mutations.

It could, with a large condition: the plan may survive even when its low-level handles do not.

What I tested

This is a direct implementation test, not a live-game benchmark or a comparison between models. I rechecked the current controller source and ran its tests on August 30, 2026. The focused core and client set passed 30 tests and 23 parameterized subtests. The complete current suite passed 170 tests and 43 subtests, with one skipped test.

The small turn simulation began with three cards at indices 0, 1, and 2. A fake planner returned one bounded plan using card IDs and a target entity ID rather than coordinates or remembered hand positions. The executor played the card currently identified as Strike. The fake game removed it and reindexed the remaining hand, just as a mutable collection would. Before the next input, the executor read state again and resolved Defend to its new index. The assertions confirmed one planner call, three accepted actions, and the expected index change.

A second regression injected an emergency stop after the first action in the same three-action plan. The result reported one executed action. The remaining card and end_turn inputs never reached the fake game.

Other tests cover changes that are less tidy than a shifting hand. If another player changes combat state while the model is planning, the first input is blocked. If multiplayer state changes between two planned actions, execution stops and asks for a new plan. A pause, disarm, mode change, lease expiry, or emergency stop increments a generation counter, so queued work from the old control state cannot become current again merely because the controller is later rearmed.

The evidence has limits. These tests use fake local game and model endpoints. They prove the controller’s validation and cancellation behavior under the simulated mutations; they do not prove compatibility with every live game state, successful cooperative play, or a particular wall-clock latency improvement. I did not make a billable model call or automate a live run for this post.

Intent can be stable when handles are not

The useful split is between what the model means and what the executor must touch.

The model can say, in effect, “play the first playable Strike on that living enemy.” That intent may remain sensible after the previous card resolves. A hand index such as 1 is only an implementation detail from one observation. Treating the index as part of the plan gives a temporary fact a longer lifetime than it deserves.

The controller therefore accepts a stable card ID plus an occurrence number for duplicates. Just before dispatch, ordinary code finds the matching playable card in the current hand and uses its current index. Targets get checked against the current set of living entity IDs. The runtime also confirms that combat still belongs to the local player’s play phase and that the act, floor, and round have not changed.

Fresh state does not automatically require fresh model reasoning. Sometimes the new observation says that the old intent is still legal and only its concrete representation changed. In that case, deterministic translation is enough. If the observation changes the meaning of the choice, the executor stops instead of improvising around the model.

That distinction matters for latency. Calling a model after every mechanical mutation adds delay even when the next decision has not changed. Blindly replaying a whole queue is faster, but spends safety on assumptions that the first action may have invalidated. One semantic plan plus fresh resolution keeps the expensive reasoning call at the level of the turn while moving state-sensitive checks into code that can run before every input.

The plan is bounded too. The validator permits at most 12 allowlisted actions. Non-combat decisions are limited to one action per plan, and end_turn must be last. Those rules keep batching attached to a narrow case where several actions can express one tactical intention. They do not grant the model a general script runner.

A model response is a proposal, not a queue

I would use the same pattern outside games. A browser agent can propose “open the result with this stable record ID” while the executor locates its current row. A deployment agent can name a service and desired state while ordinary code resolves the current container identity and checks the revision. In both cases, the proposal can remain useful after the interface around it changes.

This changes how I want tool contracts written. Model output should carry semantic references and bounded intent. The executor should own ephemeral indices and current authorization; it must serialize dispatch and honor stop state. Every action that can mutate those facts creates a new validation point.

The three-card test did not make the plan immutable. The tactical idea lasted for the turn, but each concrete handle was resolved again from current state.