Skip to content

Give each party member their own place on a battle's range track - #216

Merged
mmacy merged 2 commits into
mainfrom
chunk/positions
Oct 31, 2026
Merged

mmacy merged 2 commits into
mainfrom
chunk/positions

Conversation

@mmacy

@mmacy mmacy commented Oct 31, 2026

Copy link
Copy Markdown
Owner

This is the positions chunk of issue #209. Each party member now has their own place on a battle's range track. A close moves only the member who declares it, at their own encounter rate, and a fighting withdrawal or a retreat by one member moves that member away. Each move reports a MemberMovedEvent. The ranks toward a group come from distance, then arrival order, then marching order. When every visible member stands behind the party line, the line moves back to the foremost of them. A formation retreat's chase starts from the member nearest the monsters.

replacement_id now only trades marching-order places, so battle.declaration.no_replacement and battle.declaration.replacement_too_far are gone.

A battle in which nobody moves draws exactly as before. The phase 2, 3, and 11 to 15 goldens are unchanged, and a comparison of 400 such battles against main gave identical event logs. The phase 4 and 5 goldens and the example crawler fail under their non-strict markers until the monster-reach chunk stops groups from walking past members, and the goldens chunk regenerates them. Moving and attacking in one round, monster reach, and the view fields are later chunks, and their tests stay marked.

https://claude.ai/code/session_01V7ErtnsmhaP7aLwcfmzohD

mmacy added 2 commits October 30, 2026 23:23
Each member now stands at an advance from the party line, in BattleState.advance_feet, and BattleState.arrived_round records the round they got there. A member's distance to a group is the group's distance minus the member's advance.

- A close moves only the member who declares it, at their own encounter rate, doubled by haste on them. It stops 5' from the named group and short of 5' from every other group still in the fight, and never takes a member back.
- A fighting withdrawal or a retreat that not every declarer makes moves that member back alone, at half or full rate, below the line if need be. Any member can make one, and it needs no replacement.
- replacement_id is a trade of marching-order places. The trade happens right after the withdrawing member's move, only when that move happens and the named member is alive. The named member has to be another declarer who isn't falling back too and isn't named twice.
- MemberMovedEvent reports each member's own move: the members who fall back first, then the members who close, each run in marching order.
- A formation move still moves the groups and no member. A formation retreat's pursuit starts at the smallest distance from a living member to a group that can follow.
- At the end of the movement step, when every living member who isn't invisible stands behind the line, the line moves back to the foremost of them, with no event.
- The ranks toward a group are the living members sorted by distance, then arrival round, then marching order, cut into rows of the formation width that never mix distances. member_distance_feet and group_front_rank report the distance and the front rank within 5'.
- A melee attack or a staff of striking is judged at the member's own distance: combat.attack.out_of_reach beyond 5', then battle.declaration.not_in_front_rank for a member within 5' outside the front rank. The pre-phase judges it with the round's trades made. The melee step resolves an attack only when the member still stands there, or when the group runs and the member stood within 5' of it at acceptance.
- The default weapon, the melee or missile choice, a missile's range, a thrown item's range, and a confused member's nearest group read the member's own distance. The default policies close only while no living member stands within 5' of the group.

battle.declaration.no_replacement and battle.declaration.replacement_too_far are no longer raised, so they leave the source and tools/docs/rejection_codes.json. The descriptions of not_in_front_rank and replacement_unavailable now state the new rules.

Every step is integer arithmetic on state, with no draw. In a battle where nobody moves, the distances are the group distances, the ranks are the marching-order ranks, and the line never moves, so the draws are the same as before. The phase 2, 3, and 11 to 15 goldens pass unchanged.

Claude-Session: https://claude.ai/code/session_01V7ErtnsmhaP7aLwcfmzohD
…row member gets

The positions tests pass, so their expected-to-fail markers come off.

Two contract tests disagreed about a member who is both out of reach and outside the front row. The contract checks reach first, in the BattleDeclaration.move docstring, the EncounterGroupView.front_rank docstring, and the melee fizzle's reasons. So the test of the member alone in front now expects combat.attack.out_of_reach for Hild at 30', and checks that Nim alone is the front rank.

The player-view property test asserted the old key set of a group view, so it failed whenever Hypothesis generated an encounter. It now includes the four fields the contract added.

Claude-Session: https://claude.ai/code/session_01V7ErtnsmhaP7aLwcfmzohD
@mmacy
mmacy merged commit d106eff into main Oct 31, 2026
5 checks passed
@mmacy
mmacy deleted the chunk/positions branch October 31, 2026 06:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant