Files
SirRoboGarage/docs/tfil_hold_budget_j154.md
T
SirStone 2223ca667e 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.
2026-09-27 10:23:46 +02:00

7.3 KiB
Raw Blame History

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".