Arena: sim-rate map and target section 7 (sim rate cut, step part of A-pure)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a487f6c0a4
commit
a19d0eb73b
2 changed files with 212 additions and 4 deletions
200
docs/aetheria-arena-sim-rate-map.md
Normal file
200
docs/aetheria-arena-sim-rate-map.md
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
# Aetheria arena, cut sim-rate: one game tick, lowered for game and arena alike
|
||||
|
||||
Date: 2026-10-22. Status: map, drafted by Imagination for Self to commit beside `docs/aetheria-arena-target.md`.
|
||||
Body pinned at `GameCult/Aetheria` origin/master `0267a191` (view-seam merged). Every claim below was probed on that tree.
|
||||
Spec: `aetheria-arena:cut_spec:cut-sim-rate.r1`. Question: `aetheria-arena:question:sim-tick-rate`.
|
||||
|
||||
## 1. What the operator said
|
||||
|
||||
On 2026-10-21, while we were talking about Colosseum throughput (session transcript `53e9c70e`, lines 4811-4879):
|
||||
|
||||
- "So we can expect 160x realtime for the Colosseum? That's not bad, we can gather a few thousand matches per hour per
|
||||
thread on Yggdrasil, then?"
|
||||
- "I think we can fudge the time step just a little, as long as there's enough resolution for all the relevant decisions"
|
||||
- "Projectiles are roll based, their flight is pure presentation, I made that decision partly with this in mind"
|
||||
- "Physics definitely doesn't need 60, not at the pace we're setting. I don't think it even needs 30."
|
||||
- Answering Self's proposal (lower the whole game's step, chosen by a timer check, an equivalence test and her playtest,
|
||||
landing before the runner): "Go for it."
|
||||
|
||||
That is ruling `aetheria-arena:ruling:lower-game-sim-rate`. Her words decide two things: physics needs less than 60 Hz,
|
||||
and probably less than 30. The proposal she approved decides the rest: the game's own rate is lowered (`A-one-sim` holds
|
||||
unchanged), and the rate is chosen by the timer check, the equivalence test and her playtest.
|
||||
|
||||
Rulings already in force on the same clock (2026-10-07, campaign `aetheria-release`):
|
||||
- `sim-speed-knob`: the sim's own clock owns pace; Unity's time scale is not touched.
|
||||
- `sim-speed-surface`: a design speed in game data times the player's accessibility slow-down.
|
||||
- `sim-speed-tenth`: the design speed will be about a tenth of today's; the tick rate stays hidden; the player's ship may be
|
||||
extrapolated.
|
||||
- `tick-rate-follows-design-speed`: the step follows the design speed, so the real-time tick rate is a chosen constant.
|
||||
"6hz is the kind of rate players notice. But we definitely don't *need* 60."
|
||||
- `tick-30hz-floor-half`: "30 Hz with 0.5 floor". TickRate is 30 Hz at design speed. The player's factor ranges from 0.5 to 1,
|
||||
so the slowest tick is 15 Hz.
|
||||
|
||||
The design-speed and accessibility cut (`cut-sim-speed-scale.r2`) was withdrawn under `map-only-what-runs`. So on master the
|
||||
game still runs at design speed 1 and 60 ticks per real second. The 30 Hz in `tick-30hz-floor-half` never landed. Her
|
||||
2026-10-21 words ("I don't think it even needs 30") go below it, which is the fork in section 6.
|
||||
|
||||
## 2. Body facts
|
||||
|
||||
### Where the rate lives
|
||||
|
||||
- **Two hidden 60s.** `ActionGameManager.Clock = new SimClock(1f / 60f)` (`ActionGameManager.cs:251`) is the only clock
|
||||
that steps the game. `SimClock`'s constructor defaults `stepsPerRealSecond = 60` (`SimClock.cs:21`). The two are
|
||||
independent: changing the step to 1/20 while leaving the default would run 60 steps of 1/20 s per real second, which
|
||||
makes the game three times faster. The 60 is decided twice, by two writers that do not know about each other.
|
||||
- **ServerShared has no game rate.** `Zone.Update(float deltaTime)` (`Zone.cs:356`) steps by whatever it is given. Every
|
||||
sim rule integrates the `dt` it is handed. Nothing in ServerShared knows the game's step.
|
||||
- **The headless drivers choose their own step.** There is no runner yet. The tests that pin game histories step with
|
||||
literals: the two-AI digest (`DeterminismLedgerTests.cs:217`, `arena.Update(1f / 60f)`, 3600 steps) and the Duel history
|
||||
(`SimClockTests.OneHistoryTwice`, `ClockFrame = 1f / 60f`, `:134`). The tests contain 64 `1f / 60` literals, 31 of them
|
||||
`Update(1f / 60f)` calls. Most are physics fixtures that need some small step, not the game's step.
|
||||
- **Unity's fixed timestep is not the sim.** No `FixedUpdate` or `Rigidbody` exists in `Assets/Scripts`. The sim steps
|
||||
only through `Clock.Advance(Time.deltaTime, ...)` (`ActionGameManager.cs:1333`). `ProjectSettings/TimeManager.asset`
|
||||
decides nothing about the sim and is left alone.
|
||||
|
||||
### What assumes 60 Hz
|
||||
|
||||
The question is not whether a rule mentions 60. It is whether the rule's per-second meaning depends on the step.
|
||||
|
||||
- **Timers that drop their overshoot.** These rules restart a normalized countdown at 1 and discard what the last step
|
||||
overshot:
|
||||
- `InstantWeapon.Execute` (`InstantWeapon.cs:227-265`): the cooldown ends when `_cooldown < 0`. The next burst starts
|
||||
only when a pilot triggers it again on a later step.
|
||||
- `Cooldown.cs:51,60`.
|
||||
- `Sensor` ping cooldown (`Sensor.cs:144-148,183`).
|
||||
- `ConstantWeapon` ammo interval (`ConstantWeapon.cs:148-151`).
|
||||
|
||||
For the cooldown this works out to `floor(C/dt) + 1` steps per cycle, not `C/dt`. A 0.25 s cooldown fires every 16
|
||||
steps at 60 Hz (0.267 s, 6.7% slow) and every 6 steps at 20 Hz (0.30 s, 20% slow). Damage per second is therefore a
|
||||
function of the tick rate today, at 60 Hz as well. This is the rule that would make an equivalence test fail for the
|
||||
wrong reason.
|
||||
- **Timers that already carry.** Two rules keep their overshoot, so their per-second rates survive any step:
|
||||
- the beam's roll cadence (`ConstantWeapon.cs:185-196`, `while (_beamTimer >= interval) _beamTimer -= interval`);
|
||||
- the burst's round cadence (`InstantWeapon.cs:243-270`, several rounds in one step if the interval is shorter).
|
||||
|
||||
Transfer-verb's give budget (`CargoTransfer.cs:80-94` on its branch) carries too.
|
||||
- **Passive sensing is a forward-Euler decay.** `Sensor.Execute` applies `next = saturate(previous + G dt)` and then
|
||||
`next *= 1 - TargetInfoDecay * dt` (`Sensor.cs:225-231`). Its fixed point is `G (1 - k dt) / k`, not the `G / k` that
|
||||
`Entity.PassiveInfo` documents and returns (`Entity.cs:518-532`). The bias is `k dt`: detection ranges shrink as the
|
||||
step grows. The exact form, `x e^{-k dt} + (G/k)(1 - e^{-k dt})`, settles at `G / k` at every step. A ping is an impulse
|
||||
with no `dt` (`Sensor.cs:251-254`) and is step-free.
|
||||
- **Rules that are step-free already.**
|
||||
- Drag is `decay(v, drag, delta)` (`Ship.cs:132`).
|
||||
- Shots are rolls: commit and arrival are sim times checked each step (`FireControl.Step`, `CommitHorizon = .5 s`), so a
|
||||
resolution lands up to one step late, never differently.
|
||||
- Tunnelling does not exist for rounds (ruling `projectile-flight-is-presentation`).
|
||||
- One-tick notifications (paints, ping crossings, `IncomingPaints`) are defined per tick and read per tick
|
||||
(`ActionGameManager.cs:1336-1340`).
|
||||
- **Integration error that remains.** Ship motion is stepped explicitly. At a larger step, a hard-turning ship's arc and the
|
||||
in-arc and in-range test at commit move by O(dt). This is the residual her "fudge" admits. The equivalence test measures
|
||||
it.
|
||||
- **Hull contact.** `Contacts.Step` tests overlap after every move. A closing speed times the step that exceeds the
|
||||
smallest hull's radius can pass through. The cut checks the catalog's fastest ship against its smallest hull at the
|
||||
chosen step.
|
||||
|
||||
### How the render covers the step
|
||||
|
||||
`DrawAhead` (`DrawAhead.cs:13-19,114-139`) draws every moving thing at `T = Zone.ExactTime + Clock.Lead`:
|
||||
- closed-form poses (orbits, belts, the field) at T;
|
||||
- integrated poses (ships, bodies) as the last step plus step velocity times `Lead`;
|
||||
- the ship's heading turned by `TurnRate * Lead`.
|
||||
|
||||
This is extrapolation (dead reckoning), not interpolation. It adds no latency, and presenters store no previous state, by
|
||||
rule. At 20 Hz, `Lead` reaches 50 ms. The visible cost is a snap whenever velocity or turn rate changes between steps:
|
||||
about `Δω * Lead` in heading (a full turn-rate reversal of 3 rad/s gives 0.15 rad at 20 Hz, against 0.05 at 60). Positions
|
||||
move by `½ a Lead²`, which is negligible.
|
||||
|
||||
Input reaches the sim through `PilotCommand`. Held fields persist, and one-shots queue in order until the next step applies
|
||||
them (`Pilot.cs:43-102`). A tap shorter than a step still fires, because a pull and then a release in one step both apply,
|
||||
in order. The step costs the player up to one step of input lag: 33 ms at 30 Hz, 50 ms at 20 Hz.
|
||||
|
||||
### What the digest pins
|
||||
|
||||
`TwoAiDigestIsPinned` pins one SHA-256 over 3600 per-step `Snapshot`s of the two-AI fight at `1/60`, for host class
|
||||
`linux-x64` (`DeterminismLedgerTests.cs:31-34,186-249`). It has been repinned twice, each for a stated cause. This cut
|
||||
repins it once, for two causes landing together: the game step (the history has a different number of steps of a
|
||||
different length), and the carried timers and exact sensor decay (cadence and detection change at any step). It still
|
||||
pins the same simulated length: 60 s, which is `60 * TickRate` steps. `OneHistoryTwice` and `TwoAiFightIsOneHistory`
|
||||
compare two runs and need no pin.
|
||||
|
||||
### Measured on Yggdrasil
|
||||
|
||||
**Not measured yet.**
|
||||
- **The probe is ready.** `SimRateProbe.cs` sits beside this map. It runs a catalog timer survey and the two-AI fight over
|
||||
120 simulated seconds at 60, 30, 20, 15 and 10 Hz, three reps each after a warm-up, and reports the best wall time,
|
||||
shots, hits, first hull loss, first death and survivors.
|
||||
- **Why it has not run.** It waited about an hour for a Yggdrasil slot. Then three runs failed in the fixture: the test
|
||||
catalog now loads the first-party mod `aetheria.nakovalnya`, whose `ship.cc` and `ship.glb` are LFS objects the old
|
||||
probe recipe did not resolve. With every pointer under `GameData` resolved, one is still missing.
|
||||
- **The recipe to use.** transfer-verb's job script resolves LFS for current master
|
||||
(`F:\Projects\eureka-handoff-aux-2026-10-19\imag-nak-r2\job.sh`).
|
||||
- **What the earlier probe showed.** At d52b50f9 the Duel ran about 9,700 steps/s at 60 Hz, mostly with one ship alive.
|
||||
- **Expectation, not a measurement.** The cost per step does not depend on dt, so cost per simulated second scales with
|
||||
the tick rate: 2x at 30, 3x at 20, 4x at 15.
|
||||
|
||||
## 3. Prior art
|
||||
|
||||
- **Glenn Fiedler, "Fix Your Timestep!" (2004).** Use a fixed `dt`, an accumulator fed by real time, a clamp against the
|
||||
spiral of death, and then draw either by interpolating the previous and current states (smooth, one step of added
|
||||
latency) or by extrapolating. `SimClock` already is the accumulator and the clamp (`MaxFrameSeconds = .1`), and
|
||||
`DrawAhead` is the extrapolating branch.
|
||||
- **Supreme Commander (2007)** runs its sim at 10 ticks per second and draws interpolated, for fleets of hundreds. It is
|
||||
the closest precedent for a large-unit, non-twitch game at a low tick.
|
||||
- **StarCraft II** runs 16 game loops per sim second (22.4 per real second at Faster), with deterministic lockstep and
|
||||
interpolated rendering.
|
||||
- **Factorio** is fixed at 60 UPS, with every timer counted in ticks and lockstep multiplayer, and no decoupling of sim and
|
||||
render rate. It is the counter-example: it keeps 60 because its belts and inserters are tick-quantised content. Ours are
|
||||
per-second content.
|
||||
- **EVE Online** runs its Destiny ball physics on a 1 Hz server tick, and clients interpolate and predict. It shows that
|
||||
tactical space combat at a slow pace tolerates very coarse ticks when the player commands intent rather than twitch.
|
||||
- **X4** steps the sectors the player is in at frame rate and the others at a much coarser, abstracted rate. That is the
|
||||
living-world campaign's concern, not this cut's.
|
||||
|
||||
The lesson that carries over: content authored in seconds survives a step change only if every timer and integrator is
|
||||
step-exact. Fiedler's accumulator gives that for the clock, and section 2's two families (timers that drop overshoot,
|
||||
Euler decay) are the places where Aetheria does not have it yet.
|
||||
|
||||
## 4. Authority map
|
||||
|
||||
- **Owner.** `SimClock` owns the game's tick: `SimClock.TickRate` (steps per real second at design speed 1) and
|
||||
`SimClock.GameStep = 1f / TickRate` (sim seconds per step). `SimClock.ForGame()` builds the game's clock from both.
|
||||
- **Inputs.** None at run time. It is a code constant, so it is part of the code revision in `A-pure`'s tuple.
|
||||
- **Outputs.** The `dt` every game-rate driver hands to `Zone.Update`, and `Lead`.
|
||||
- **Derived.**
|
||||
- `ActionGameManager.Clock` is `SimClock.ForGame()`.
|
||||
- The digest, the Duel history and later the runner step by `SimClock.GameStep`.
|
||||
- A timer's cadence and a sensor's settling level are functions of seconds, not of steps.
|
||||
- **Demoted.**
|
||||
- `SimClock`'s `stepsPerRealSecond` default is no longer an owner. The parameter becomes required, and the game's value is
|
||||
derived from `TickRate`.
|
||||
- `1f / 60f` in `ActionGameManager` is no longer an owner; it is derived from `SimClock.GameStep`.
|
||||
- Physics fixtures may keep a literal step of their own, because they test physics, not the game's history.
|
||||
- **Forbidden writers.**
|
||||
- Any `new SimClock(` in `Assets/Scripts` other than `ForGame`.
|
||||
- Any literal step in a driver of a pinned game history.
|
||||
- A timer that resets to 1 and discards its overshoot.
|
||||
- A per-step decay written as `1 - k * dt`.
|
||||
- **Shared paths.** The player's game, the digest and Duel histories, and the future runner all step `Zone.Update` by
|
||||
`SimClock.GameStep`. The design-speed cut, when re-mapped, sets `Step = DesignSpeed / TickRate` inside `ForGame` and
|
||||
touches nothing else.
|
||||
- **Deletion line.** The default `= 60` and the `1f / 60f` in `ActionGameManager` go first. The overshoot-dropping resets go
|
||||
before the rate changes, so the rate change is measured on step-exact rules.
|
||||
|
||||
## 5. Not this cut
|
||||
|
||||
- **Brains deciding below the physics step.** The ruling allows it, and `PilotCommand`'s held fields already make it
|
||||
possible. No brain-rate machinery lands here.
|
||||
- **Design speed and the player's slow-down** (`sim-speed-scale`, withdrawn). `ForGame` is the one place where it will
|
||||
land.
|
||||
- **Render interpolation.** Extrapolation is ruled (`sim-speed-tenth`, DrawAhead's no-stored-state rule). Her playtest is
|
||||
where a heading snap would show. If it does, interpolating the player's ship only is the follow-up, at one step of
|
||||
latency.
|
||||
- **Unity's `TimeManager` fixed timestep**, which the sim does not read.
|
||||
|
||||
## 6. The fork
|
||||
|
||||
`aetheria-arena:question:sim-tick-rate`: which tick rate Hands lands for her playtest, given `tick-30hz-floor-half`.
|
||||
Options and recommendation are in the question. In short:
|
||||
- 30 Hz keeps the in-force ruling and halves the cost.
|
||||
- 20 Hz follows her 2026-10-21 words and costs a third, but at the 0.5 accessibility floor the real tick is 10 Hz.
|
||||
- 15 Hz is a quarter of the cost and 7.5 Hz at the floor, near the 6 Hz she called noticeable.
|
||||
|
|
@ -85,7 +85,8 @@ Harness:
|
|||
|
||||
- `A-pure`: A match is a pure function of (code revision, catalog revision, scenario id, match seed, brain ids per
|
||||
side, host determinism class). The same inputs give the same per-tick state digest, in any process, run alone or
|
||||
in a batch.
|
||||
in a batch. The step is part of the code revision: `SimClock.GameStep`, never a
|
||||
driver's own literal.
|
||||
- `A-no-clock`: No input to a match comes from the wall clock, process identity, hash-code ordering of reference
|
||||
types, or a stream another subsystem advances. Every draw a match reaches comes from `SimulationDice` (or a successor
|
||||
owner) keyed by a structured seed. A missing seed is refused, never defaulted to the clock.
|
||||
|
|
@ -169,10 +170,17 @@ No cell is empty. The Scenario row exposes a gap: `Name` is today's only identit
|
|||
3. **View seam.** Add a `PilotView` built from sensors and book. Brains lose their live `Ship`, `Zone` and `ItemManager`
|
||||
references. `Flight` and `Minion` become `DoctrineBrain` behind an `IBrain` registry. Aiming moves to the view if
|
||||
`observation-fidelity` rules for it.
|
||||
4. **Replay.** The recorder writes inputs, commands and digests every N ticks as CultCache. The replayer reproduces
|
||||
4. **Sim rate** (ruling `lower-game-sim-rate`, map `docs/aetheria-arena-sim-rate-map.md`, spec `cut-sim-rate.r1`).
|
||||
- One game tick, `SimClock.TickRate`, lowered for game and arena alike, so `A-one-sim` holds unchanged.
|
||||
- Before it, timers carry their overshoot and passive sensing decays exactly, so content authored in seconds means the
|
||||
same thing at any tick.
|
||||
- The two-AI digest is repinned once, at the game step, for those two causes.
|
||||
- It lands before replay and the runner, so recorded digests and measured throughput are at the real rate.
|
||||
- The rate is the one question `sim-tick-rate` decides, then the equivalence test, then her playtest.
|
||||
5. **Replay.** The recorder writes inputs, commands and digests every N ticks as CultCache. The replayer reproduces
|
||||
them and names the first divergent tick. Replay tests read an existing match.
|
||||
5. **Runner.** `AetherDb arena`: a scenario, two brain ids and a seed range produce results and replays in a given
|
||||
6. **Runner.** `AetherDb arena`: a scenario, two brain ids and a seed range produce results and replays in a given
|
||||
directory. It runs in Yggdrasil's dotnet container, with two-ship throughput measured and recorded.
|
||||
6. **First tournament.** Round-robin over two or more brains (`DoctrineBrain` plus one variant) and the combat
|
||||
7. **First tournament.** Round-robin over two or more brains (`DoctrineBrain` plus one variant) and the combat
|
||||
scenarios, with enough seeds per pairing. Ratings are derived with confidence intervals, and the results document
|
||||
is ready to feed `aetheria-matchups`.
|
||||
|
|
|
|||
Loading…
Reference in a new issue