Skip to content

Let a member close and attack in one round, and dry-run a planned round - #217

Merged
mmacy merged 3 commits into
mainfrom
chunk/move-and-attack
Oct 31, 2026
Merged

mmacy merged 3 commits into
mainfrom
chunk/move-and-attack

Conversation

@mmacy

@mmacy mmacy commented Oct 31, 2026

Copy link
Copy Markdown
Owner

This is the move-and-attack chunk of issue #209. An attack declaration can set move="close", so the member closes in the movement step and attacks in the same round, as the OSE SRD allows: "Movement and attacking may be combined in the same round." A thief can close and back-stab in the free round after a surprise. A cast with any move is refused with battle.declaration.cast_cannot_move, and another action with a move is refused with battle.declaration.move_not_combinable.

Each round is now judged against the positions that all its declared moves produce, so a member who steps into a place another member left can attack in that round. preview_battle_round runs the same judgement on a partial or complete plan, with no draw and no change, so a battle screen can grey out a choice the round would refuse. A melee attack that position stops reports a DeclarationFizzledEvent with the code battle.melee.fizzled. A spell's range is measured from the caster's own place.

A battle in which nobody moves draws as before: the phase 2, 3, and 11 to 15 goldens are unchanged. Monster reach and the view fields are later chunks.

https://claude.ai/code/session_01V7ErtnsmhaP7aLwcfmzohD

mmacy added 3 commits October 30, 2026 23:53
…declares

An attack can now set move="close". The member closes in the party's movement step and attacks in the missile or the melee step of the same round. So a thief can close and back-stab in the free round after a surprise, and a fighter can close and swing in one round.

The pre-phase now projects every declared move before it judges any declaration: each member's close, each fall-back a member makes alone with its trade of marching-order places, a formation move, and the move of the party line. It judges each attack at the member's projected distance, chooses the default weapon there, and judges each melee attack against the front rank those moves produce. So a member who steps into the place another member leaves can strike in the same round. The movement step carries out the same plan, which _plan_member_moves builds for both.

preview_battle_round runs the same pre-phase as the round, through _judge_round, on any subset of the declarers, and leaves out the roster check. It returns each declaration's rejections, each living member's projected advance, and each group's projected front rank. It changes nothing and takes no draw.

A cast that sets a move gets battle.declaration.cast_cannot_move. A turn_undead, use_item, or hold that sets one, and an attack with a fighting_withdrawal or a retreat, get battle.declaration.move_not_combinable. The session checks them after the back-stab checks and before the action's own. An attack that closes, by a member who can't move, gets battle.declaration.cannot_move. tools/docs/rejection_codes.json describes the two new codes.

A melee attack that can't resolve where the member stands at the melee step now reports a player-visible DeclarationFizzledEvent at battle.melee.fizzled, with the group's id. An attack that closes from beyond 5' to within 5' stays a melee attack. So when the group runs first and the close falls short, the attack fizzles instead of turning into a throw.

The ResolveBattleRound.declarations docstring no longer says the party closes on the group of the first close.

In a battle where no member moves, the projection is the battle as it stands, so the default weapons and the judgements are the same as before, and no attack closes. The phase 2, 3, and 11 to 15 goldens pass unchanged.

Claude-Session: https://claude.ai/code/session_01V7ErtnsmhaP7aLwcfmzohD
_cast_targets returned the group's distance from the party line as the distance a spell's range is checked against. With members at their own places on the range track, that's wrong for a caster who stands forward of the line or behind it. It now returns the caster's own distance to the group, or to the farthest group a targeted monster belongs to. So a magic-user who closed 40' alone can cast magic missile at goblins 190' from the line, which stand 150' from them.

The pre-phase, a scroll read, and the magic phase all read the distance this way. A caster can't move in the round they cast, so the battle as it stands gives the same distance as the pre-phase's projection. In a battle where no member moves, every advance is zero and the distance is the group's distance, as before.

Claude-Session: https://claude.ai/code/session_01V7ErtnsmhaP7aLwcfmzohD
The tests for closing and attacking in one round, the dry run, the new rejection codes, and the ResolveBattleRound example pass, so their expected-to-fail markers come off.

Claude-Session: https://claude.ai/code/session_01V7ErtnsmhaP7aLwcfmzohD
@mmacy
mmacy merged commit 435376f into main Oct 31, 2026
5 checks passed
@mmacy
mmacy deleted the chunk/move-and-attack branch October 31, 2026 07:00
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