Spend speed as a rate, not as a tiebreak #37

Merged
icub3d merged 1 commit from 6-turn-scheduler into main 2026-09-13 18:21:13 +00:00
Owner

Design pillar 2 says turn order is a resource: speed decides how often a unit acts, not
merely who acts first. A round with an initiative sort inside it cannot express that —
everybody acts once regardless, so the gap between 2 speed and 20 speed is the same as
between 2 and 3, and "building for tempo" is not a strategy the game has.

The shape

Every unit carries an interval, the virtual time between its turns, derived once from
its TurnSpeed. The schedule hands the next turn to whoever is due soonest, then pushes
that unit one interval on. A unit of twice the speed takes twice the turns.

Virtual time is integral. CADENCE is 720720 — the LCM of 1 through 16 — so every speed
in that range divides it exactly and a cadence is a precise ratio rather than a rounded
one. Larger speeds round once, at insertion, and the error never accumulates because an
interval is computed rather than added up.

Ties go to the lower Deployed ordinal, a new component recording the mission's
authored deployment order. (due, ordinal) is a total order because ordinals are
distinct, so there is exactly one answer and it never depends on how the ECS stored
anything. Breaking ties by Entity would have been free and wrong — entity ids come from
spawn order and reuse, which is precisely what § Determinism forbids depending on.

A unit's next turn is an absolute instant, not a queue position. That is what makes
the awkward cases fall out rather than need rules: a casualty removes one entry and the
survivors keep the turns they were already due; a reinforcement waits its own interval
instead of arriving owed a backlog it never took.

upcoming copies the schedule and runs the real advance, so the lookahead the HUD draws
(#10) cannot disagree with the turns that follow it.

Acceptance criteria

  • Order asserted against a hand-computed sequence — speeds 2 and 1 give
    A A B A A B A A B, worked through in the test's comment.
  • Double speed acts twice as often — asserted as exactly fast == slow * 2 over
    300 turns, not approximately. A second test covers speeds that divide nothing neatly
    (13 against 7) and holds the ratio to within 1%.
  • Ties are stable and documented — lower Deployed ordinal, with a test proving
    the result does not follow the order units were added in.
  • Removing a unit preserves relative order — the survivors' sequence is compared
    against the same schedule with the casualty filtered out.
  • Lookahead matches advancing — upcoming(12) compared element-wise against 12
    actual advances, taken part-way through a run.

Verification

cargo fmt --check, cargo clippy --all-targets -- -D warnings, and cargo test —
140 passing, up from 122. Thirteen tests on the rules, five on CombatPlugin.

src/combat/schedule.rs has no ECS in it beyond Entity and is tested without an App,
the same discipline as world::path and for the same reason: this is the part that will
rot silently.

Ran on a Pixel Tablet — new plugin, so it touches startup; enters a mission and takes
touch input with no panic.

ADR 0007

Records the shape and what it costs. The largest cost is unpaid and worth reading before
this merges: there are no rounds any more. Nothing can last "two rounds", so a status
effect (M3) will have to be measured in its bearer's own turns or in virtual time, and
effects will have to say which.

Nothing spends a turn yet — that is #8.

Closes #6

🤖 Generated with Claude Code

https://claude.ai/code/session_01TA4hJHkRSU3XBxZYtMKXdh

Design pillar 2 says turn order is a resource: speed decides how *often* a unit acts, not merely who acts first. A round with an initiative sort inside it cannot express that — everybody acts once regardless, so the gap between 2 speed and 20 speed is the same as between 2 and 3, and "building for tempo" is not a strategy the game has. ## The shape Every unit carries an **interval**, the virtual time between its turns, derived once from its `TurnSpeed`. The schedule hands the next turn to whoever is due soonest, then pushes that unit one interval on. A unit of twice the speed takes twice the turns. Virtual time is integral. `CADENCE` is 720720 — the LCM of 1 through 16 — so every speed in that range divides it exactly and a cadence is a precise ratio rather than a rounded one. Larger speeds round once, at insertion, and the error never accumulates because an interval is computed rather than added up. **Ties go to the lower `Deployed` ordinal**, a new component recording the mission's authored deployment order. `(due, ordinal)` is a total order because ordinals are distinct, so there is exactly one answer and it never depends on how the ECS stored anything. Breaking ties by `Entity` would have been free and wrong — entity ids come from spawn order and reuse, which is precisely what § Determinism forbids depending on. A unit's next turn is an **absolute instant**, not a queue position. That is what makes the awkward cases fall out rather than need rules: a casualty removes one entry and the survivors keep the turns they were already due; a reinforcement waits its own interval instead of arriving owed a backlog it never took. `upcoming` copies the schedule and runs the real `advance`, so the lookahead the HUD draws (#10) cannot disagree with the turns that follow it. ## Acceptance criteria - [x] **Order asserted against a hand-computed sequence** — speeds 2 and 1 give `A A B A A B A A B`, worked through in the test's comment. - [x] **Double speed acts twice as often** — asserted as *exactly* `fast == slow * 2` over 300 turns, not approximately. A second test covers speeds that divide nothing neatly (13 against 7) and holds the ratio to within 1%. - [x] **Ties are stable and documented** — lower `Deployed` ordinal, with a test proving the result does not follow the order units were added in. - [x] **Removing a unit preserves relative order** — the survivors' sequence is compared against the same schedule with the casualty filtered out. - [x] **Lookahead matches advancing** — `upcoming(12)` compared element-wise against 12 actual advances, taken part-way through a run. ## Verification `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings`, and `cargo test` — **140 passing**, up from 122. Thirteen tests on the rules, five on `CombatPlugin`. `src/combat/schedule.rs` has no ECS in it beyond `Entity` and is tested without an `App`, the same discipline as `world::path` and for the same reason: this is the part that will rot silently. Ran on a Pixel Tablet — new plugin, so it touches startup; enters a mission and takes touch input with no panic. ## ADR 0007 Records the shape and what it costs. The largest cost is unpaid and worth reading before this merges: **there are no rounds any more.** Nothing can last "two rounds", so a status effect (M3) will have to be measured in its bearer's own turns or in virtual time, and effects will have to say which. Nothing spends a turn yet — that is #8. Closes #6 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01TA4hJHkRSU3XBxZYtMKXdh
Design pillar 2 says turn order is a resource: speed decides how often a unit
acts, not merely who acts first. A round with an initiative sort inside it
cannot express that — everybody acts once regardless, so the gap between 2 speed
and 20 speed is the same as between 2 and 3, and building for tempo is not a
strategy the game has.

So every unit carries an interval, the virtual time between its turns, derived
once from its `TurnSpeed`. The schedule hands the next turn to whoever is due
soonest and pushes that unit one interval on. A unit of twice the speed takes
twice the turns, exactly.

Virtual time is integral. `CADENCE` is 720720, the least common multiple of 1
through 16, so every speed in that range divides it exactly and a cadence is a
precise ratio rather than a rounded one. Larger speeds round once, at insertion,
and the error never accumulates because an interval is computed rather than
added up.

Ties go to the lower `Deployed` ordinal — a new component recording the
mission's authored deployment order. `(due, ordinal)` is a total order because
ordinals are distinct, so there is exactly one answer and it never depends on
how the ECS stored anything. Breaking ties by `Entity` would have been free and
wrong: entity ids come from spawn order and reuse, which is the thing
`CLAUDE.md` § Determinism forbids outcomes from depending on.

A unit's next turn is an absolute instant rather than a position in a queue,
which is what makes the awkward cases fall out instead of needing rules. A
casualty removes one entry and the survivors keep the turns they were already
due. A reinforcement waits its own interval instead of arriving owed a backlog
it never took.

`upcoming` copies the schedule and runs the real `advance`, so the lookahead the
HUD draws (#10) cannot disagree with the turns that follow it. A second
implementation that predicts is the kind of thing that stays right for a year
and then quietly does not.

`src/combat/schedule.rs` has no ECS in it beyond `Entity`, and its rules are
tested without an `App` — the same discipline as `world::path`, for the same
reason: this is the part that will rot silently. Thirteen tests on the rules and
five on `CombatPlugin`, which keeps the resource in step with the units that
exist.

ADR 0007 records the shape and what it costs. The largest cost is unpaid: there
are no rounds any more, so nothing can last "two rounds" and a status effect
(M3) will have to be measured in its bearer's own turns or in virtual time.

Nothing spends a turn yet. That is #8.

Closes #6

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TA4hJHkRSU3XBxZYtMKXdh
icub3d merged commit d71cb53a4c into main 2026-09-13 18:21:13 +00:00
icub3d deleted branch 6-turn-scheduler 2026-09-13 18:21:14 +00:00
Sign in to join this conversation.
No description provided.