j154 (default-off): the DERIVED bounded hold TR_TFIL_HOLD_MAX_TICKS + panic release
Budget derived from the server source (tank-royale 0.35.5): calcGunHeat(p) = 1 + p/5, coolDown 0.1/tick, fire only at heat == 0, calcBulletDamage(3.0) = 16 -> two max-power shots are 16 ticks apart and 32 damage is the most the enemy can land in that window (brute force over the 0.1 power grid: 8/11/16/24/32 ticks -> 16/18/32/32/48). 16 = CommitTicks + 1, the first window that admits the enemy's second shot. Knob defaults to 0 = today's behaviour byte-for-byte (checked over 20026 ticks). Hold is taken only on a replan tick with an empty safe set, at most N ticks per streak, released the tick a safe tile exists, counter reset on the pick. PANIC RELEASE: a tracked bullet whose closest approach is within its core at t* in [0, min(N,16)] overrides the hold immediately. The gun is untouched. Also fixes a stray '&' that stopped test_tfil_commit_env.nim from compiling at all, and enforces the 'a hold never interrupts a live commitment' invariant j153 documented but did not implement. Registers TR_TFIL_HOLD_MAX_TICKS in env_report + knownEnvNames + .env.example. No battle, no A/B run.
This commit is contained in:
@@ -114,6 +114,7 @@ TR_TFIL_TILE_REPLAN=self # self | enemy | off: when a dodge commitment is can
|
|||||||
TR_TFIL_COMMIT_TICKS=15 # ticks to commit to a dodge point before replanning
|
TR_TFIL_COMMIT_TICKS=15 # ticks to commit to a dodge point before replanning
|
||||||
TR_TFIL_ARRIVE_TICKS=0.0 # hard bound: never pick a tile farther than this (ticks at 8px/tick); 0 = off = today's draw
|
TR_TFIL_ARRIVE_TICKS=0.0 # hard bound: never pick a tile farther than this (ticks at 8px/tick); 0 = off = today's draw
|
||||||
TR_TFIL_HOLD_WHEN_TRAPPED=off # on = when NO safe tile exists, hold position one tick instead of taking the 2 least-hot blocked tiles; off = today's fallback
|
TR_TFIL_HOLD_WHEN_TRAPPED=off # on = when NO safe tile exists, hold position one tick instead of taking the 2 least-hot blocked tiles; off = today's fallback
|
||||||
|
TR_TFIL_HOLD_MAX_TICKS=0 # BOUNDED version of the above: hold at most N ticks per empty-safe-set streak (default 0 = off = today's fallback). The budget is DERIVED from the enemy's rate of fire (2 x 3.0-power shots = 16 ticks); the hold is released the tick a safe tile exists and overridden outright if a tracked bullet reaches us within min(N, 16) ticks
|
||||||
TR_TFIL_DANGER_THRESHOLD=10.0 # hard heat filter on the path; lava is quantised to 5, so the effective steps are 10/15/20 and 10-14 admits exactly what 10 does
|
TR_TFIL_DANGER_THRESHOLD=10.0 # hard heat filter on the path; lava is quantised to 5, so the effective steps are 10/15/20 and 10-14 admits exactly what 10 does
|
||||||
TR_TFIL_DIAG=off # on = fill the per-pick picker loss histogram (TfilLoss*); observability only
|
TR_TFIL_DIAG=off # on = fill the per-pick picker loss histogram (TfilLoss*); observability only
|
||||||
TR_TFIL_GEO_MODE=off # off | turn | dist | both, each with a `-soft` (default) / `-topk` / `-rej` suffix; shapes the DRAW over the heat-filtered pool
|
TR_TFIL_GEO_MODE=off # off | turn | dist | both, each with a `-soft` (default) / `-topk` / `-rej` suffix; shapes the DRAW over the heat-filtered pool
|
||||||
|
|||||||
@@ -317,6 +317,8 @@ proc printEffectiveValues(ctx: EnvReportContext) =
|
|||||||
sourceOf("TR_TFIL_ARRIVE_TICKS"))
|
sourceOf("TR_TFIL_ARRIVE_TICKS"))
|
||||||
emit("TR_TFIL_HOLD_WHEN_TRAPPED", onOff(the_floor_is_lava.TfilHoldWhenTrapped),
|
emit("TR_TFIL_HOLD_WHEN_TRAPPED", onOff(the_floor_is_lava.TfilHoldWhenTrapped),
|
||||||
sourceOfPresence("TR_TFIL_HOLD_WHEN_TRAPPED"))
|
sourceOfPresence("TR_TFIL_HOLD_WHEN_TRAPPED"))
|
||||||
|
emit("TR_TFIL_HOLD_MAX_TICKS", $the_floor_is_lava.TfilHoldMaxTicks,
|
||||||
|
sourceOf("TR_TFIL_HOLD_MAX_TICKS"))
|
||||||
emit("TR_TFIL_WALL_HOTNESS", $the_floor_is_lava_ring.WallHotness,
|
emit("TR_TFIL_WALL_HOTNESS", $the_floor_is_lava_ring.WallHotness,
|
||||||
sourceOf("TR_TFIL_WALL_HOTNESS"))
|
sourceOf("TR_TFIL_WALL_HOTNESS"))
|
||||||
emit("TR_TFIL_WALL_RADIANCE", $the_floor_is_lava.WallRadiance,
|
emit("TR_TFIL_WALL_RADIANCE", $the_floor_is_lava.WallRadiance,
|
||||||
@@ -662,7 +664,7 @@ proc knownEnvNames*(): seq[string] =
|
|||||||
"TR_TFIL_RANGE_LO", "TR_TFIL_RANGE_HI", "TR_TFIL_RANGE_TEMP",
|
"TR_TFIL_RANGE_LO", "TR_TFIL_RANGE_HI", "TR_TFIL_RANGE_TEMP",
|
||||||
"TR_TFIL_RANGE_K", "TR_TFIL_CORRIDOR_HEAT", "TR_TFIL_WALL_HOTNESS",
|
"TR_TFIL_RANGE_K", "TR_TFIL_CORRIDOR_HEAT", "TR_TFIL_WALL_HOTNESS",
|
||||||
"TR_TFIL_CORRIDOR_TICKS", "TR_TFIL_ARRIVE_TICKS",
|
"TR_TFIL_CORRIDOR_TICKS", "TR_TFIL_ARRIVE_TICKS",
|
||||||
"TR_TFIL_HOLD_WHEN_TRAPPED",
|
"TR_TFIL_HOLD_WHEN_TRAPPED", "TR_TFIL_HOLD_MAX_TICKS",
|
||||||
"TR_TFIL_DANGER_THRESHOLD", "TR_TFIL_DIAG",
|
"TR_TFIL_DANGER_THRESHOLD", "TR_TFIL_DIAG",
|
||||||
"TR_TFIL_GEO_MODE", "TR_TFIL_GEO_TAU",
|
"TR_TFIL_GEO_MODE", "TR_TFIL_GEO_TAU",
|
||||||
"TR_TFIL_WALL_RADIANCE", "TR_TFIL_BULLET_CORE", "TR_TFIL_BULLET_AURA",
|
"TR_TFIL_WALL_RADIANCE", "TR_TFIL_BULLET_CORE", "TR_TFIL_BULLET_AURA",
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# j154 — the DERIVED hold budget for "hold when the safe set is empty"
|
||||||
|
|
||||||
|
**Status: implemented, DEFAULT OFF, no battle run.** `TR_TFIL_HOLD_MAX_TICKS`
|
||||||
|
(default `0` = today's behaviour byte-for-byte). The knob, the panic release and
|
||||||
|
the guards live in `common_libs/movements/the_floor_is_lava.nim` and
|
||||||
|
`common_libs/tests/test_tfil_commit_env.nim`.
|
||||||
|
|
||||||
|
## 1. The mechanics, re-verified against the server source
|
||||||
|
|
||||||
|
Source used: **the server Kotlin sources at `/home/davide/Projects/tank-royale`
|
||||||
|
(v0.35.5)**, not a cached doc. There is no `docs/energy_math.md` in this repo.
|
||||||
|
|
||||||
|
| Fact | Value | Source |
|
||||||
|
|---|---|---|
|
||||||
|
| gun heat added per shot | `1 + p/5` | `server/.../rules/math.kt:125` `calcGunHeat` |
|
||||||
|
| gun cooling | `0.1` / tick | `core/GunEngine.kt:108` `coolDownGun`, default `DEFAULT_GUN_COOLING_RATE = 0.1` (`lib/common/.../RuleDefaults.kt:25`) |
|
||||||
|
| may fire only at | `gunHeat == 0.0` (strict; the else-branch cools instead) | `core/GunEngine.kt:36` |
|
||||||
|
| bullet damage | `4p`, `+2(p-1)` above 1 → `6p-2` | `rules/math.kt:112-118` `calcBulletDamage` |
|
||||||
|
| firepower clamp | `0.1 … 3.0` | `rules/rules.kt:49,52` |
|
||||||
|
| round-start gun heat | `3.0` | `rules/rules.kt:22` `INITIAL_GUN_HEAT` |
|
||||||
|
|
||||||
|
Note: there is no `MaxGunHeat = 3.0` gate in this server — the fire gate is
|
||||||
|
`gunHeat == 0`, and `3.0` is only the *initial* heat a bot starts a round with
|
||||||
|
(30 idle ticks of cooldown). The 16-tick number below is unchanged by that
|
||||||
|
distinction, because it is derived from the heat ADD and the cooling rate.
|
||||||
|
|
||||||
|
## 2. The owner's frame: "the time between shooting 2 × 3.0-power bullets"
|
||||||
|
|
||||||
|
`heat(3.0) = 1 + 3/5 = 1.6`; `1.6 / 0.1` = **16 ticks** between two max-power
|
||||||
|
shots. `calcBulletDamage(3.0) = 6*3 - 2 = ` **16**, so two of them = **32**.
|
||||||
|
|
||||||
|
The *fastest* repeat is a `0.1`-power shot: `1.02` heat → **11 ticks**
|
||||||
|
(the 11th subtraction is what takes the residual 0.02 to 0), for
|
||||||
|
`4*0.1 = ` **0.4** damage. So "2 × 3.0-power" is a **DAMAGE** bound, not a
|
||||||
|
COUNT bound: the enemy can fire ~1.5× as often, but each of those shots is 40×
|
||||||
|
weaker. The right question is therefore "how much damage can land in N ticks",
|
||||||
|
not "how many bullets".
|
||||||
|
|
||||||
|
## 3. Max damage deliverable in N ticks (brute force over the power quantisation)
|
||||||
|
|
||||||
|
DP over the 0.1-step power grid (30 powers), the enemy free to mix powers
|
||||||
|
(it may interleave weak shots to shorten its own interval), first shot free at
|
||||||
|
t = 0:
|
||||||
|
|
||||||
|
| N (ticks) | max total damage | how |
|
||||||
|
|---|---|---|
|
||||||
|
| 8 | **16** | one 3.0 shot; the 0.1-power repeat needs 11 |
|
||||||
|
| 11 | **18** | 3.0 (16) at t=0, then 0.5-power (2) at t=11 |
|
||||||
|
| 16 | **32** | 3.0 at t=0 and t=16 — two max shots |
|
||||||
|
| 24 | **32** | same two; the next shot cannot land before t=32 |
|
||||||
|
| 32 | **48** | 3.0 at t=0, 16, 32 |
|
||||||
|
| 64 | **80** | five max shots (linear thereafter) |
|
||||||
|
|
||||||
|
The damage *rate* `(6p-2)/(10+2p)` is monotone increasing in `p` (0.036 dmg/tick
|
||||||
|
at 0.1, 0.33 at 1.0, 1.0 at 3.0), so no mix beats pure 3.0-power asymptotically;
|
||||||
|
mixing only wins at a window edge (N = 11 above), never by more than one weak
|
||||||
|
shot. **16 ticks is the exposure ceiling of a hold: 32 damage = 16 % of the
|
||||||
|
200 HP a bot carries.**
|
||||||
|
|
||||||
|
## 4. How 16 relates to the code
|
||||||
|
|
||||||
|
* `CommitTicks = 15` (`the_floor_is_lava.nim:71`). The derived budget is
|
||||||
|
**`CommitTicks + 1`**: a hold of 15 ticks admits ONE max-power shot (16
|
||||||
|
damage), 16 ticks admits the second (32). 16 is the first window in which the
|
||||||
|
enemy's *second* bullet can land at all, so it is the shortest budget that
|
||||||
|
cannot be surprised by a third. The two numbers agree by construction, which
|
||||||
|
is the point: the mover's existing commitment length and the enemy's rate of
|
||||||
|
fire are the same quantity here.
|
||||||
|
* j144 measured a **mean hold of 24.0 ticks** live. 24 sits between the 16- and
|
||||||
|
32-tick damage steps: it buys no extra protection (still 32) and is exposed to
|
||||||
|
the same two shots. A 16-tick cap is therefore a *tightening* of j144's
|
||||||
|
measured behaviour, not an extrapolation of it — hence the 24 arm in the A/B.
|
||||||
|
|
||||||
|
## 5. The knob
|
||||||
|
|
||||||
|
`TR_TFIL_HOLD_MAX_TICKS` (int, default `0` = off). When the safe tile set is
|
||||||
|
empty (~65 % of picks offline) and the mover is on a **replan** tick, it holds
|
||||||
|
position for at most N ticks per empty streak. Rules:
|
||||||
|
|
||||||
|
* a safe tile exists → release on the same tick (no latency);
|
||||||
|
* counter resets when a safe tile is taken, so the bound is per streak;
|
||||||
|
* the hold never interrupts a live commitment (it replaces a replan only) —
|
||||||
|
j153's comment claimed this and its code did not enforce it; j154 does;
|
||||||
|
* the **gun is untouched**: `computeMove` never emits fire, and `ModularBot`
|
||||||
|
aims and fires from tracked state after `go()` on every tick. Guarded: the
|
||||||
|
fire detector still latches the enemy's wave on a held tick.
|
||||||
|
|
||||||
|
### Panic release (required)
|
||||||
|
|
||||||
|
`bulletPanic(m, selfX, selfY, horizon)` — for each tracked bullet, closest
|
||||||
|
approach of its straight path is `t* = ((self-b)·v)/|v|²`; the hold is
|
||||||
|
overridden when `0 ≤ t* ≤ horizon` and the miss distance is within the
|
||||||
|
bullet's own core radius. Horizon = **`min(N, 16)` ticks**. It reuses the
|
||||||
|
tracked ghost's own position/velocity — the same model `pathMaxHeat` decays by
|
||||||
|
and `advanceBullets` integrates — so there is no second arrival model in the
|
||||||
|
file (`TR_TFIL_ARRIVE_TICKS` is a *tile-selection* filter, not a bullet-arrival
|
||||||
|
predictor). `min(N, 16)` because a bullet arriving after the budget expires
|
||||||
|
cannot hurt a hold that has already ended; 16 is the derived exposure window.
|
||||||
|
|
||||||
|
### Composition with the other knobs
|
||||||
|
|
||||||
|
* `TR_TFIL_COMMIT_ARRIVAL=1` — **wins**. A hold is only reachable on a replan
|
||||||
|
tick, so under arrival the bot is in a commitment and never holds. The
|
||||||
|
arrival commitment is a hold on a *destination*; the empty-set hold is a hold
|
||||||
|
with *no destination*. They never compete.
|
||||||
|
* `TR_TFIL_NOREV_SPEED=4` — no interaction: it filters mid-flight candidate
|
||||||
|
switches inside the pick block, which a hold short-circuits, and it cannot
|
||||||
|
force a pick during a hold.
|
||||||
|
* `TR_TFIL_ARRIVE_TICKS` — no interaction: it only filters a **non-empty** safe
|
||||||
|
set, and the hold only triggers when that set is empty.
|
||||||
|
|
||||||
|
## 6. Guards
|
||||||
|
|
||||||
|
`common_libs/tests/test_tfil_commit_env.nim`, `testJ154` (15 checks): default 0;
|
||||||
|
explicit `0` byte-for-byte the unset build over 20 026 ticks; knob parsing
|
||||||
|
(16 / junk / negative); the N-tick bound; same-tick release when a safe tile
|
||||||
|
appears; counter reset and budget refill; the gun still fires while holding; the
|
||||||
|
held command is the same stop as at-target; panic release; panic specificity
|
||||||
|
(a bullet 200 px off the line still holds); the horizon check; the hold never
|
||||||
|
interrupts a live commitment; clearing the knob.
|
||||||
|
|
||||||
|
Suite total: **136 PASS** (121 before j154, not the 99 quoted in the brief —
|
||||||
|
see the note below).
|
||||||
|
|
||||||
|
## 7. Proposed A/B — NOT RUN
|
||||||
|
|
||||||
|
Arms: `TR_TFIL_HOLD_MAX_TICKS` = 0 (control) / 16 / 24; `TR_MOVEMENT=tfil`
|
||||||
|
pinned; frozen 15-opponent panel; everything else at shipped defaults.
|
||||||
|
Primary metrics: **damage per run and round-win rate** (never hit rate).
|
||||||
|
Mechanism metrics: % picks held, incoming hit rate while holding, panic-release
|
||||||
|
frequency, distance-to-enemy at the moment of the hold.
|
||||||
|
|
||||||
|
MDE: ~0.28 wins/run at ~14 runs/arm (two arms ≈ 1 h); resolving ~0.10
|
||||||
|
wins/run needs ~2.2 h ≈ 2 900 battles for a three-arm design.
|
||||||
|
|
||||||
|
Expectation to state up front: four mechanism-positive / outcome-null results in
|
||||||
|
a row (j152 geometry, j151 arrival bound, j144 arrival commitment, j153 hold)
|
||||||
|
make a null the likely outcome, and this is the fifth candidate. 1 h can only
|
||||||
|
say "no large effect"; ~2.2 h is needed before "no small effect".
|
||||||
Reference in New Issue
Block a user