j152 (default-off): geometry shapes the tile DRAW (TR_TFIL_GEO_MODE/TAU) + the offline sweep with the diversity cost
The owner: choose the tile pool not only from the heat point but from the geometric position too. Heat stays the hard filter; the draw over the survivors is re-weighted by turn and/or distance. Three weighting forms (soft softmax / top-K third / rejection band), one env name carrying both axes. Default = off = today's uniform draw, byte-for-byte (golden parity). WHAT DIFFERS FROM j9 (TR_TFIL_TURN_BIAS, a live null): that was a tiebreak weight among the non-empty safe set only. This runs on the WHOLE pool the draw already runs on, including the 2 promoted least-hot tiles the ~65% forced picks choose from. OFFLINE (8 fixtures x 3 seeds, no java): headline 'tile actually reached at tta' 4.5% -> 16.9% at TR_TFIL_GEO_MODE=both-soft TR_TFIL_GEO_TAU=45, with no diversity collapse (distinct tiles 220 -> 219, normalised entropy 0.87 -> 0.86, top-tile share 7.0% -> 8.6%). perpE — the perpendicular rate in the FORCED population — does not move in ANY arm: that pool is 2 tiles ranked by heat alone and the only lever is a coin flip. Also registers j150's TR_TFIL_DIAG / TR_TFIL_DANGER_THRESHOLD in env_report + knownEnvNames + .env.example (test_env_report was red) and documents DANGER_THRESHOLD as quantisation-limited: heat comes in 5s, so the effective steps are 10/15/20 and 10-14 admits zero extra tiles. No battle, no server, no A/B run.
This commit is contained in:
@@ -70,6 +70,15 @@ var
|
||||
|
||||
const CommitTicks = 15 ## ticks to commit to a dodge point
|
||||
const MinCommitTicks = 5 ## must commit for this many ticks before danger replan allowed
|
||||
## j154: the DERIVED hold budget's panic horizon. Server `rules/math.kt`:
|
||||
## calcGunHeat(p) = 1 + p/5, coolDown 0.1/tick, and a gun may only fire at
|
||||
## heat == 0 (`core/GunEngine.kt:36`), so two MAX-power shots are 1.6/0.1 = 16
|
||||
## ticks apart and a 3.0-power bullet is `calcBulletDamage(3) = 16`. 16 ticks
|
||||
## is therefore the window in which the enemy can land AT MOST its next two
|
||||
## shots (32 damage) on us — the whole exposure a hold can possibly buy. It is
|
||||
## also the FIRST window that admits the enemy's second shot at all, so a
|
||||
## hold shorter than this can never be surprised by a third bullet.
|
||||
const HoldPanicTicks = 16.0
|
||||
const DangerReplanThreshold = 25.0 ## replan on serious threats only (bullet core), not corridors/auras
|
||||
const CoolestLevels = 2 ## how many distinct lava values count as "cool"
|
||||
const MaxTrackedBullets = 20 ## hard cap on tracked bullets
|
||||
@@ -95,6 +104,33 @@ type
|
||||
rrNone, rrInit, rrTileSelf, rrTileEnemy, rrDanger, rrExpiry,
|
||||
rrArrival, rrHyst ## j144: the tile was REACHED / the alternative won by a margin
|
||||
|
||||
type
|
||||
TfilGeoDim* = enum ## WHAT geometry the draw is shaped by
|
||||
gdoOff = "off", gdoTurn = "turn", gdoDist = "dist", gdoBoth = "both"
|
||||
TfilGeoShape* = enum ## HOW the shape is turned into a draw
|
||||
gfSoft = "soft", gfTopK = "topk", gfRej = "rej"
|
||||
|
||||
proc parseGeo*(s: string): tuple[dim: TfilGeoDim, form: TfilGeoShape] =
|
||||
## `"turn"`, `"dist"`, `"both"` | any of those + `"-soft"` (default) |
|
||||
## `"-topk"` | `"-rej"`. Anything unrecognised, and `"off"`, is OFF = today's
|
||||
## uniform draw. One env name, two axes: a `both-soft`/`both-topk` grid would
|
||||
## need three names for the same three dials.
|
||||
var a = s.strip().toLowerAscii()
|
||||
var form = gfSoft
|
||||
let dash = a.rfind('-')
|
||||
if dash > 0:
|
||||
case a[dash + 1 .. ^1]
|
||||
of "topk": form = gfTopK; a = a[0 ..< dash]
|
||||
of "rej", "rejection": form = gfRej; a = a[0 ..< dash]
|
||||
of "soft": a = a[0 ..< dash] # the default form, spelled out
|
||||
else: discard
|
||||
result.form = form
|
||||
result.dim = case a
|
||||
of "turn": gdoTurn
|
||||
of "dist", "distance": gdoDist
|
||||
of "both": gdoBoth
|
||||
else: gdoOff
|
||||
|
||||
proc tileReplanName*(m: TfilTileReplan): string =
|
||||
case m
|
||||
of ttrSelf: "self"
|
||||
@@ -163,11 +199,47 @@ var
|
||||
## the DEFAULT is today's `10.0`, so the default path is bit-identical.
|
||||
## TR_TFIL_DANGER_THRESHOLD default 10.0
|
||||
TfilDangerThreshold* = DefaultDangerThreshold
|
||||
## j152: `TR_TFIL_GEO_MODE` / `TR_TFIL_GEO_TAU` — GEOMETRY shapes the DRAW.
|
||||
## Heat still gates the pool with the same hard filter; this only re-weights
|
||||
## the survivors by how far the tile sits from where we are already going.
|
||||
## TR_TFIL_GEO_MODE off | turn | dist | both [+ `-soft` | `-topk` | `-rej`]
|
||||
## TR_TFIL_GEO_TAU deg, 0 = off (= today's uniform draw, exactly)
|
||||
## WHAT IS DIFFERENT FROM j9 (`TR_TFIL_TURN_BIAS`, a live null): that was a
|
||||
## tiebreak WEIGHT applied only among the non-empty safe set. This runs on the
|
||||
## WHOLE pool the draw already runs on, so it also shapes the 2 promoted
|
||||
## least-hot tiles the ~65% forced (safePre < 2) picks choose from.
|
||||
TfilGeoMode*: TfilGeoDim = gdoOff
|
||||
TfilGeoForm*: TfilGeoShape = gfSoft
|
||||
TfilGeoTau*: float = 0.0
|
||||
## j151: `TR_TFIL_ARRIVE_TICKS` — refuse a candidate we cannot REACH inside
|
||||
## the commitment horizon (ticks = dist / MaxSpeed). Hard bound, not a
|
||||
## preference; empty pool => today's full pool, so it can never starve the
|
||||
## draw. 0 (default) = off = byte-for-byte today.
|
||||
TfilArriveTicks*: float = 0.0
|
||||
## j153: `TR_TFIL_HOLD_WHEN_TRAPPED` — when the SAFE set is EMPTY (zero tiles
|
||||
## with `pathMaxHeat <= PathDangerThreshold` inside the reachable hull), STOP
|
||||
## for this tick instead of promoting the 2 least-hot blocked tiles. The
|
||||
## owner's rule: "if no tile is found to go, to not choose the less dangerous,
|
||||
## but to stay still! the next tick probably the situation already changed".
|
||||
## ONE tick only, never latched: the hold is taken at the pick site, and the
|
||||
## pick site only runs when `commitTicks == 0`, so the very next tick
|
||||
## re-evaluates the field from scratch. That is why there is no max-hold knob:
|
||||
## a counter can only add a way to get stuck.
|
||||
## Default false = byte-for-byte today's promote-the-2 behaviour.
|
||||
TfilHoldWhenTrapped*: bool = false
|
||||
## j154: `TR_TFIL_HOLD_MAX_TICKS` — the BOUNDED version of the j153 one-tick
|
||||
## hold. While the safe tile set stays EMPTY the mover holds position for at
|
||||
## most this many ticks (the enemy's own rate of fire bounds what waiting can
|
||||
## buy: see `HoldPanicTicks`). Rules, all four of them load-bearing:
|
||||
## * a safe tile exists -> release on the SAME tick, always
|
||||
## * a tracked bullet reaches us within `min(N, 16)` ticks -> PANIC RELEASE,
|
||||
## the hold is overridden and the normal promote-the-2 fallback resumes
|
||||
## * the counter resets when a safe tile is taken, so the bound is per
|
||||
## empty-streak, not per round
|
||||
## * the gun is untouched: `computeMove` never fires, so a held tick still
|
||||
## fires exactly as every other tick (verified in `test_tfil_commit_env`)
|
||||
## Default 0 = OFF = byte-for-byte today's behaviour, j153 included.
|
||||
TfilHoldMaxTicks*: int = 0
|
||||
## j150: `TR_TFIL_DIAG` — fill `TfilLoss*` with the per-pick LOSS HISTOGRAM
|
||||
## (how many tiles die at each picker stage). Pure counters, off by default.
|
||||
TfilDiag*: bool = false
|
||||
@@ -228,12 +300,18 @@ proc loadTfilCommitEnv*() =
|
||||
TfilDangerThreshold = max(0.0, getEnvFloat("TR_TFIL_DANGER_THRESHOLD",
|
||||
DefaultDangerThreshold))
|
||||
TfilDiag = getEnvBool("TR_TFIL_DIAG", false)
|
||||
let (gd, gf) = parseGeo(getEnv("TR_TFIL_GEO_MODE", "off"))
|
||||
TfilGeoMode = gd
|
||||
TfilGeoForm = gf
|
||||
TfilGeoTau = max(0.0, getEnvFloat("TR_TFIL_GEO_TAU", 0.0))
|
||||
# j151: hard arrival bound. The draw is UNIFORM over every safe tile inside the
|
||||
# 50-tick reachable hull, so a tile 47 ticks away had the same 1-in-52 chance
|
||||
# as the adjacent one, while the target is only HELD for CommitTicks=15. The
|
||||
# offline ruler (measure_tfil_pick_defects) measured 65% of picks beyond the
|
||||
# 15-tick horizon and a 6.5% arrival rate. 0 = off = today's uniform draw.
|
||||
TfilArriveTicks = max(0.0, getEnvFloat("TR_TFIL_ARRIVE_TICKS", 0.0))
|
||||
TfilHoldWhenTrapped = getEnvBool("TR_TFIL_HOLD_WHEN_TRAPPED", false)
|
||||
TfilHoldMaxTicks = max(0, getEnvInt("TR_TFIL_HOLD_MAX_TICKS", 0))
|
||||
if not TfilDiag: TfilLoss = TfilLossStats()
|
||||
|
||||
loadTfilCommitEnv()
|
||||
@@ -394,6 +472,12 @@ type
|
||||
## candidate set of the last pick (j145:
|
||||
## lets a caller measure the REGRET of the
|
||||
## draw instead of only the drawn value)
|
||||
lastHeld: bool ## the last tick HELD position
|
||||
## (j153: TR_TFIL_HOLD_WHEN_TRAPPED,
|
||||
## the safe set was empty)
|
||||
holdTicks: int ## j154: ticks HELD in the current
|
||||
## empty-safe-set streak; reset to 0
|
||||
## when a safe tile is taken
|
||||
lastPickSafe: int ## how many SAFE tiles (pathMaxHeat
|
||||
## <= PathDangerThreshold) the last pick
|
||||
## drew from (j146: the size of the set
|
||||
@@ -428,6 +512,7 @@ proc resetRound*(m: var TFILModule) =
|
||||
m.fire.reset()
|
||||
m.commitTicks = 0
|
||||
m.commitAge = 0
|
||||
m.holdTicks = 0 ## j154: the bounded hold never survives a round
|
||||
m.cachedHull = @[]
|
||||
m.cachedInsideTiles = @[]
|
||||
m.blockedTile = (col: 0, row: 0, active: false)
|
||||
@@ -443,6 +528,7 @@ proc resetRound*(m: var TFILModule) =
|
||||
m.picks = 0
|
||||
m.lastPickPromoted = false
|
||||
m.lastPickMinTurn = 0.0
|
||||
m.lastHeld = false
|
||||
m.lastPickSafe = 0
|
||||
|
||||
# ── Commit diagnostics (TR_TFIL_COMMIT_LOG, off by default) ──────────────────
|
||||
@@ -546,6 +632,28 @@ proc detectFires(m: var TFILModule, ws: WorldState) =
|
||||
" eta=", sqrt((ws.selfX - b.x)^2 + (ws.selfY - b.y)^2) / sp
|
||||
m.fire.endScan()
|
||||
|
||||
proc bulletPanic*(m: TFILModule, selfX, selfY, horizon: float): bool =
|
||||
## PANIC RELEASE (j154). True when a TRACKED bullet's straight path comes
|
||||
## within its own CORE of where WE are at any time in `[0, horizon]` ticks.
|
||||
## Closest approach of a straight ray is `t* = ((self - b) . v) / |v|^2`; the
|
||||
## `t* < 0` case is the `dot < 0` reap in `advanceBullets` (it is already past
|
||||
## us) and `t* > horizon` is "not inside the window". The prediction is the
|
||||
## tracked ghost's OWN position/velocity — the same model `pathMaxHeat` decays
|
||||
## by and `advanceBullets` integrates — so there is no second arrival model in
|
||||
## this file. Exported so the guard test can call it directly.
|
||||
if horizon <= 0.0: return false
|
||||
for b in m.bullets:
|
||||
let v2 = b.velX * b.velX + b.velY * b.velY
|
||||
if v2 < 1e-9: continue
|
||||
let dx = selfX - b.x
|
||||
let dy = selfY - b.y
|
||||
let t = (dx * b.velX + dy * b.velY) / v2
|
||||
if t < 0.0 or t > horizon: continue
|
||||
let mx = dx - t * b.velX
|
||||
let my = dy - t * b.velY
|
||||
if sqrt(mx * mx + my * my) <= bulletRadii(b.power).core: return true
|
||||
false
|
||||
|
||||
proc advanceBullets(m: var TFILModule, selfX, selfY: float) =
|
||||
## Advance positions and reap bullets that are: passed us, out of bounds, or too old.
|
||||
var i = 0
|
||||
@@ -725,6 +833,76 @@ proc turnWeights*(turns: openArray[float], bias, refDeg: float): seq[int] =
|
||||
result.add max(1, int(round(1.0 + bias *
|
||||
(1.0 - max(0.0, t - refDeg) / 180.0))))
|
||||
|
||||
# ── j152: the geometric DRAW ────────────────────────────────────────────────
|
||||
# The owner: "choose tiles pool not only from the heat point but from a
|
||||
# geometrically position too". Heat is already a HARD filter (unchanged); this is
|
||||
# the second half — the distribution the draw samples from.
|
||||
#
|
||||
# WHY THIS IS NOT j9 AGAIN. j9 (`TR_TFIL_TURN_BIAS`) down-weighted the turn among
|
||||
# the non-empty safe set and measured a live null. This runs on the pool the draw
|
||||
# ALREADY runs on, which for ~65% of picks is the 2 promoted least-hot tiles that
|
||||
# broke the heat filter — j9 could not see those at all.
|
||||
const GeoDegPerTick = 12.0 ## distance cost, in "effective degrees": a 15-tick
|
||||
## trip (the commitment horizon) costs the same as a
|
||||
## 180 deg turn, so ONE tau knob means the same
|
||||
## thing in `turn` and `dist` mode.
|
||||
|
||||
proc geoCosts*(turns, ttas: openArray[float], dim: TfilGeoDim): seq[float] =
|
||||
## Per-candidate cost in effective degrees. Never filters: it only re-orders
|
||||
## and re-weights tiles that already passed the heat filter.
|
||||
for i in 0..<turns.len:
|
||||
result.add (if dim in {gdoTurn, gdoBoth}: turns[i] else: 0.0) +
|
||||
(if dim in {gdoDist, gdoBoth}: ttas[i] * GeoDegPerTick else: 0.0)
|
||||
|
||||
proc geoPick*(turns, ttas: openArray[float], dim: TfilGeoDim, form: TfilGeoShape,
|
||||
tau: float): int =
|
||||
## Draw index from `candidates` under the geometric weight. NEVER returns -1
|
||||
## and NEVER returns an out-of-range index, so no arm can starve the pick.
|
||||
let c = geoCosts(turns, ttas, dim)
|
||||
var best = 0
|
||||
for i in 1..<c.len:
|
||||
if c[i] < c[best]: best = i
|
||||
case form
|
||||
of gfRej:
|
||||
# Rejection sampling: a GEOMETRY-FREE acceptance test (no shape function at
|
||||
# all) — uniform draw, redraw while the candidate costs more than `tau`.
|
||||
# ponytail: 16 tries is a fixed budget; widen it if the band ever tightens
|
||||
# enough that the fallback below starts dominating.
|
||||
for _ in 0..<16:
|
||||
let i = rand(c.high)
|
||||
if c[i] <= tau: return i
|
||||
return best # band too tight: take the best available, never starve
|
||||
of gfTopK:
|
||||
# Hard: keep the best THIRD, uniform inside. Collapses diversity by design —
|
||||
# measured against the soft form before it could ever be a default.
|
||||
let k = max(1, (c.len + 2) div 3)
|
||||
var pool: seq[int]
|
||||
var taken = newSeq[bool](c.len)
|
||||
for _ in 0..<k:
|
||||
var b = -1
|
||||
for i in 0..<c.len:
|
||||
if not taken[i] and (b < 0 or c[i] < c[b]): b = i
|
||||
taken[b] = true
|
||||
pool.add b
|
||||
return pool[rand(pool.high)]
|
||||
of gfSoft:
|
||||
# w = exp(-cost / tau), NORMALISED so the best tile weighs exactly 1.0. The
|
||||
# normalisation is what makes "all tiles tie" (and only that) degrade to the
|
||||
# uniform draw, and makes starvation impossible.
|
||||
var w: seq[float]
|
||||
for x in c: w.add exp(-x / tau)
|
||||
var wMax = 0.0
|
||||
for x in w: wMax = max(wMax, x)
|
||||
if wMax <= 0.0: return best
|
||||
var total = 0.0
|
||||
for x in w: total += x
|
||||
let r = rand(total)
|
||||
var acc = 0.0
|
||||
for i, x in w:
|
||||
acc += x
|
||||
if r < acc: return i
|
||||
return best
|
||||
|
||||
proc computeMove*(m: var TFILModule, ws: WorldState): MoveCommand =
|
||||
if m.cols == 0:
|
||||
m.initGrid(ws.arenaWidth, ws.arenaHeight)
|
||||
@@ -978,6 +1156,7 @@ proc computeMove*(m: var TFILModule, ws: WorldState): MoveCommand =
|
||||
var pickedInterval = 0
|
||||
var pickedTurn = 0.0 # log-only: |turn| to the tile that was chosen
|
||||
var pickedPromoted = false ## log-only: the pick had to break the heat filter
|
||||
var pickedHeld = false ## j153: the safe set was EMPTY -> hold this tick
|
||||
|
||||
# Hull + inside-tiles: only recompute on replan tick (commitTicks == 0)
|
||||
type TileRef = tuple[col, row: int]
|
||||
@@ -1097,8 +1276,11 @@ proc computeMove*(m: var TFILModule, ws: WorldState): MoveCommand =
|
||||
if t.pathMaxHeat <= PathDangerThreshold: safeTiles.add t
|
||||
else: blockedTiles.add t
|
||||
let safePre = safeTiles.len # j150: the safe set BEFORE the "keep 2" promotion
|
||||
let safeEmpty = safePre == 0
|
||||
if safeTiles.len < 2:
|
||||
# Fallback: promote the least-hot blocked tiles until we have 2
|
||||
# Fallback: promote the least-hot blocked tiles until we have 2. j154 runs
|
||||
# this EVEN when a hold is armed, so the panic release has a real fallback to
|
||||
# fall back ON; the hold only ever DISCARDS the promotion, at the pick site.
|
||||
# ponytail: O(n) scan on already-sorted seq — fine for small N
|
||||
let needed = 2 - safeTiles.len
|
||||
let promote = min(needed, blockedTiles.len)
|
||||
@@ -1173,7 +1355,31 @@ proc computeMove*(m: var TFILModule, ws: WorldState): MoveCommand =
|
||||
# is the first pick of the round, which is not a switch at all.
|
||||
let midFlight = m.picks > 0 and not atTarget
|
||||
|
||||
if m.commitTicks == 0 and safeTiles.len > 0:
|
||||
# ── j153/j154: THE HOLD, decided here and nowhere else ─────────────────────
|
||||
# The safe set was EMPTY, so there is nothing good to walk to. Today's answer
|
||||
# is the promote-the-2 fallback above; the owner's answer is to stand still and
|
||||
# let the field change. It is decided HERE, after the commitment block, so
|
||||
# "are we on a replan tick?" is already answered — a hold replaces a REPLAN
|
||||
# and can never interrupt a live commitment (j153's comment said that; its
|
||||
# code did not enforce it, and a mid-commitment hold silently froze the bot).
|
||||
var doPick = m.commitTicks == 0 and safeTiles.len > 0
|
||||
if m.commitTicks == 0 and safeEmpty:
|
||||
let budgeted = TfilHoldMaxTicks > 0
|
||||
let hold =
|
||||
if budgeted:
|
||||
# The BOUNDED hold: at most N ticks per empty streak, released the tick
|
||||
# a safe tile exists, and overridden outright by an inbound bullet.
|
||||
m.holdTicks < TfilHoldMaxTicks and
|
||||
not bulletPanic(m, ws.selfX, ws.selfY,
|
||||
min(TfilHoldMaxTicks.float, HoldPanicTicks))
|
||||
else:
|
||||
TfilHoldWhenTrapped # j153's ONE-tick hold, unchanged
|
||||
if hold:
|
||||
pickedHeld = true
|
||||
doPick = false
|
||||
if budgeted: inc m.holdTicks
|
||||
|
||||
if doPick:
|
||||
# Filter out the blocked tile from candidates
|
||||
var candidates: seq[ScoredTile]
|
||||
for t in safeTiles:
|
||||
@@ -1196,7 +1402,20 @@ proc computeMove*(m: var TFILModule, ws: WorldState): MoveCommand =
|
||||
for i in keep: narrowed.add candidates[i]
|
||||
candidates = narrowed
|
||||
var chosen = 0
|
||||
if (TfilNoRev or TfilTurnBias > 0.0) and candidates.len >= 2:
|
||||
# j152: geometry shapes the DRAW, on top of the heat filter (never instead
|
||||
# of it). Off by default: with `TR_TFIL_GEO_MODE=off` this whole block is
|
||||
# skipped and the draw below is byte-for-byte today's.
|
||||
# ponytail: takes precedence over TfilNoRev/TfilTurnBias (both also default
|
||||
# off) instead of composing weights; compose if two are ever armed at once.
|
||||
let geoOn = TfilGeoMode != gdoOff and TfilGeoTau > 0.0
|
||||
if geoOn and candidates.len >= 2:
|
||||
var gturns: seq[float]
|
||||
var gttas: seq[float]
|
||||
for t in candidates:
|
||||
gturns.add t.turnDeg
|
||||
gttas.add t.arriveTicks
|
||||
chosen = geoPick(gturns, gttas, TfilGeoMode, TfilGeoForm, TfilGeoTau)
|
||||
elif (TfilNoRev or TfilTurnBias > 0.0) and candidates.len >= 2:
|
||||
# Soft preferences — down-weight, never filter, and only ever among tiles
|
||||
# that already passed the hard heat filter above:
|
||||
# arm C (TR_TFIL_NO_REV, off by default) — 3:1 forward vs rearward,
|
||||
@@ -1253,6 +1472,7 @@ proc computeMove*(m: var TFILModule, ws: WorldState): MoveCommand =
|
||||
m.commitTicks = TfilCommitTicks
|
||||
m.commitAge = 0
|
||||
m.commitLava = m.lavaAt(ct.col, ct.row)
|
||||
m.holdTicks = 0 # j154: a safe tile was taken -> the budget refills
|
||||
m.blockedTile.active = false # clear after successful pick
|
||||
|
||||
# log-only: reversal test against the travel direction
|
||||
@@ -1359,10 +1579,24 @@ proc computeMove*(m: var TFILModule, ws: WorldState): MoveCommand =
|
||||
",\"reason\":\"" & reasonName(reason) & "\",\"rev\":" &
|
||||
(if pickedRev: "1" else: "0") & ",\"mid\":" &
|
||||
(if pickedMidFlight: "1" else: "0") & ",\"interval\":" & $pickedInterval &
|
||||
",\"hold\":" & (if pickedHeld: "1" else: "0") &
|
||||
",\"ht\":" & $m.holdTicks & ## j154: ticks held in the current streak
|
||||
",\"picks\":" & $m.picks & "}")
|
||||
if pickedThisTick: m.replanReason = rrNone
|
||||
m.lastHeld = pickedHeld
|
||||
|
||||
# ── Steering ─────────────────────────────────────────────────────────────────
|
||||
# j153/j154: HOLD. `speed: 0.0` is how this module already says "stop" (the
|
||||
# already-at-target case below returns the same command), so holding needs no
|
||||
# new signal, and the hold path is byte-identical to the stop path the bot
|
||||
# already emits every time it reaches its dodge tile. The gun is NOT in this
|
||||
# module — computeMove never touches fire, and `ModularBot.nim` aims and
|
||||
# fires from tracked state AFTER `go()` on every tick regardless of the speed
|
||||
# it just commanded — so a held tick still fires exactly as before. Guarded in
|
||||
# `test_tfil_commit_env.nim` (the fire detector still latches a wave on a
|
||||
# held tick).
|
||||
if pickedHeld:
|
||||
return (speed: 0.0, turnRate: 0.0)
|
||||
let stepDx = m.commitTarget.x - ws.selfX
|
||||
let stepDy = m.commitTarget.y - ws.selfY
|
||||
let dist2 = stepDx*stepDx + stepDy*stepDy
|
||||
|
||||
Reference in New Issue
Block a user