Resolve an action into everything that happens because of it #38
No reviewers
Labels
No labels
area/ai
area/build
area/character
area/combat
area/data
area/docs
area/game
area/net
area/ui
area/world
size
l
size
m
size
s
type
bug
type
design
type
feature
type
refactor
type
test
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
icub3d/terra-redux-org!38
Loading…
Reference in a new issue
No description provided.
Delete branch "7-action-resolution"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
CLAUDE.md§ Separation of Simulation and Presentation has been a sentence rather than aboundary anything could be on one side of. This builds it.
An action goes in, a list of outcomes comes out, and nothing in between touches the world.
Why
Battleis not theWorldBattleis a snapshot: plain data plusEntity, which is an identifier rather than aworld. Resolution therefore runs in a test, on a server, or from a replay with no plugins
present — which is what the architectural bet was for. Taking it by reference also makes
"an illegal action leaves state untouched" a property of the signature rather than a
promise somebody has to keep.
The pieces
Actionis a closed enum —Move,Attack,Wait. M3's effect primitives need thesame discipline; this is the small version of that decision.
Outcomecarries the whole path walked and the target's health afterwards, soplayback (#9) replays rather than re-derives.
Rejectedcarries the numbers — how far out of range, how many points short —because greying out a control without saying why is only half of pillar 4.
forecastandresolveshare one privatecheck. A preview cannot promise what theact refuses; a test asserts they refuse the same things for the same reasons.
Battle::applyis the only statement of what an outcome does to state. The pluginresolves each pending action against the snapshot in submission order, applying outcomes
as it goes — so two actions in one frame cannot both spend the same point — then mirrors
the result onto components. It decides nothing.
The generator
CombatRngis threaded in rather than reached for, per § Determinism. It is a SplitMix64written out in full rather than a dependency:
randdoes not promise the same streambetween major versions, and a replay that silently stops agreeing because a dependency
moved is a bug nobody would think to look for. This also avoids a new dependency, per
§ Working Agreements.
Nothing draws from it yet. Damage is a fixed number, which is what "no hidden dice the
player cannot reason about" reads as. It exists so M3's first roll has one tested place to
come from. Its own tests pin the stream with known answers, so if the algorithm ever
changes, a test says so.
Acceptance criteria
script twice and asserts the same outcomes, the same end state, and the same amount
of stream spent. It also asserts at least one action succeeded, so a replay of
nothing but refusals cannot pass vacuously.
Mapand aBattledirectly. NoApp.plus a test that resolves three different refusals and asserts the battle is byte-
for-byte unchanged.
forecastreturns the damage, what would be left, andwhether it is lethal, and a test asserts those are the numbers
resolvedelivers.Verification
cargo fmt --check,cargo clippy --all-targets -- -D warnings,cargo test—163 passing, up from 146. Seventeen on resolution, six on the generator.
ADR 0008
Records the boundary and its honest cost: two representations of unit state now exist.
They are built and written back inside one system so they cannot drift across frames, but
the write-back is mechanical code that must stay correct as unit state grows. That is the
thing to watch.
Nothing submits an action yet — that is #8.
Closes #7
🤖 Generated with Claude Code
https://claude.ai/code/session_01TA4hJHkRSU3XBxZYtMKXdh
`CLAUDE.md` § Separation of Simulation and Presentation has been a sentence rather than a boundary anything could be on one side of. This builds it. An action goes in, a list of outcomes comes out, and nothing in between touches the world: resolve(&Battle, Action, &mut CombatRng) -> Result<Vec<Outcome>, Rejected> `Battle` is a snapshot rather than the `World` — plain data plus `Entity`, which is an identifier and not a world. Resolution therefore runs in a test, on a server, or from a replay with no plugins present, which is what the architectural bet was for. Taking the battle by reference also means "an illegal action leaves state untouched" is a property of the signature rather than a promise somebody has to keep. `Action` is a closed enum — `Move`, `Attack`, `Wait`. M3's effect primitives need the same discipline and this is the small version of that decision. `Outcome` describes exactly what happened, including the whole path walked and the target's health afterwards, so playback (#9) replays rather than re-derives. `Rejected` carries the numbers — how far out of range, how many points short — because greying out a control without saying why is only half of pillar 4. `forecast` and `resolve` go through one private `check`, so a preview cannot promise something the act refuses. Splitting them is exactly how an interface comes to offer a move that bounces, and a test asserts they refuse the same things for the same reasons. `Battle::apply` is the only statement of what an outcome does to state. The plugin builds a snapshot, resolves every pending action against it in submission order — applying each outcome before the next action sees it, so two actions in one frame cannot both spend the same point — and then mirrors the result onto components. It decides nothing. `CombatRng` is threaded in rather than reached for, per § Determinism. It is a SplitMix64 written out in full rather than a dependency: `rand` does not promise the same stream between major versions, and a replay that silently stops agreeing because a dependency moved is a bug nobody would think to look for. Nothing draws from it yet — damage is a fixed number, which is what "no hidden dice the player cannot reason about" reads as — so it exists to give M3's first roll one tested place to come from. Seventeen tests on resolution and six on the generator, none of them needing an `App`. The replay test asserts that the same battle, actions and seed give the same outcomes, the same end state, and the same amount of stream spent. ADR 0008 records the boundary. Its honest cost: two representations of unit state now exist, built and written back inside one system so they cannot drift across frames, but the write-back is mechanical code that has to stay correct as unit state grows. Nothing submits an action yet. That is #8. Closes #7 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TA4hJHkRSU3XBxZYtMKXdh