j165: port tfil's arrival commitment to tfil_ring, default off
The_floor_is_lava_ring carried raw commitTicks with no arrival guard and no
no-reversal guard. Port the behaviour of tfil's j144 fix on RING-SPECIFIC env
names (TR_TFIL_RING_COMMIT_ARRIVAL, TR_TFIL_RING_NOREV_SPEED) so the two forks
never share a namespace. Both default OFF: with them unset the ring mover is
byte-for-byte the pre-change mover over the whole 20026-tick fixture replay
(golden generated from git show HEAD:..., checked by tfil_ring_replay.nim).
Structural differences from tfil, all noted in the code:
* ring has no TfilTileReplanMode - the tile-crossing cancel is unconditional
self-tile, so the arrival guard is just 'not TfilRingCommitArrival'.
* ring has no replanReason enum, so the arrival/danger/expiry outcomes are a
local bool; the default-off path keeps ring's original dec/no-dec exactly.
* ring's MinCommitTicks is 0 (tfil's is 5), so the arrival branch is evaluated
from the first committed tick. Left as is: changing it would change the
default path.
* ring's ScoredTile carries no turnDeg, so the no-reversal offsets are
computed by ringTileOffTravel at the pick site.
TR_TFIL_COMMIT_MARGIN (tfil's hysteresis) is deliberately NOT ported: it is a
third knob, outside the two named, and inert at its 0.0 default.
No other tfil mechanism touched: no turn-cost tiebreak, TR_TFIL_ARRIVE_TICKS,
TR_FIRE_LAG, heat-field override, corridor bound, hold, or geometry weighting.
No default changed anywhere.
This commit is contained in:
@@ -49,6 +49,10 @@
|
||||
## TR_TFIL_CORRIDOR_HEAT default 10.0 lava per corridor-overlapping tile
|
||||
## TR_TFIL_WALL_HOTNESS default 15.0 peak wall radiance at a wall tile
|
||||
## TR_MOVEMENT_LOG=1 log band/range-class changes (not/tick)
|
||||
## TR_TFIL_RING_COMMIT_ARRIVAL default off hold the committed tile until we
|
||||
## are ON it (port of tfil's j144 fix)
|
||||
## TR_TFIL_RING_NOREV_SPEED default 0.0 px/tick; below this a mid-flight
|
||||
## switch may not turn the bot around
|
||||
## `TR_TFIL_RANGE_TEMP=0` calls plain `rand(candidates.high)` exactly as the
|
||||
## original mover did, so the same binary can serve as the control arm.
|
||||
##
|
||||
@@ -141,6 +145,36 @@ proc loadTfilRingFireEnv*() =
|
||||
TfilRingFireFix = getEnvBool("TR_FIRE_FIX", true)
|
||||
loadTfilRingFireEnv()
|
||||
|
||||
## ── j165: the ARRIVAL commitment, ported from `the_floor_is_lava.nim` ────────
|
||||
## Same BEHAVIOUR as tfil's `TR_TFIL_COMMIT_ARRIVAL` / `TR_TFIL_NOREV_SPEED`,
|
||||
## RING-SPECIFIC env names so the two forks never share a namespace by accident.
|
||||
## Both default OFF, so the default path stays byte-for-byte today's ring
|
||||
## (proved over the 20026-tick fixture replay in `test_tfil_commit_env.nim`).
|
||||
## TR_TFIL_RING_COMMIT_ARRIVAL 0/1 hold the committed tile until we are
|
||||
## ON it, instead of dropping the
|
||||
## commitment on a tile crossing
|
||||
## TR_TFIL_RING_NOREV_SPEED float while |speed| is below this, a
|
||||
## mid-flight switch to the OPPOSITE
|
||||
## side is refused (0 = off)
|
||||
const
|
||||
RingArriveRadius* = 18.0 ## "we are on the committed tile" — the same 18px
|
||||
## radius `the_floor_is_lava.nim` uses
|
||||
RingHullTicks = 50 ## the reachable-hull planning horizon (the literal
|
||||
## 50 already passed to `computeReachableHull`
|
||||
## below). Past it the committed target is no longer
|
||||
## guaranteed reachable: the stall escape.
|
||||
|
||||
var
|
||||
TfilRingCommitArrival* = false
|
||||
TfilRingNoRevSpeed* = 0.0
|
||||
|
||||
proc loadTfilRingCommitEnv*() =
|
||||
## Read the j165 knobs. Called once at module init; the guard test calls it
|
||||
## again after `putEnv` so the non-default arms run in one process.
|
||||
TfilRingCommitArrival = getEnvBool("TR_TFIL_RING_COMMIT_ARRIVAL", false)
|
||||
TfilRingNoRevSpeed = max(0.0, getEnvFloat("TR_TFIL_RING_NOREV_SPEED", 0.0))
|
||||
loadTfilRingCommitEnv()
|
||||
|
||||
const
|
||||
DefaultRangeLo = 100.0
|
||||
DefaultRangeHi = 200.0
|
||||
@@ -240,6 +274,10 @@ type
|
||||
commitTarget: tuple[x, y: float] ## world coords of committed dodge point
|
||||
commitTicks: int ## ticks remaining on commitment
|
||||
commitLava: float ## lava at commit time (for spike detection)
|
||||
commitAge: int ## j165: ticks since the current target
|
||||
## was picked (0 = just picked)
|
||||
picks: int ## j165: picks made this round; > 0 means
|
||||
## a switch would be MID-FLIGHT
|
||||
blockedTile: tuple[col, row: int; active: bool] ## excluded from next pick after danger replan
|
||||
cachedHull: seq[tuple[x, y: float]]
|
||||
cachedInsideTiles: seq[tuple[col, row: int]]
|
||||
@@ -283,6 +321,8 @@ proc resetRound*(m: var TFILRingModule) =
|
||||
m.bullets = @[]
|
||||
m.fire.reset()
|
||||
m.commitTicks = 0
|
||||
m.commitAge = 0
|
||||
m.picks = 0
|
||||
m.cachedHull = @[]
|
||||
m.cachedInsideTiles = @[]
|
||||
m.blockedTile = (col: 0, row: 0, active: false)
|
||||
@@ -462,6 +502,40 @@ proc computeReachableHull(x0, y0, heading0, speed0,
|
||||
if cur == startIdx: break
|
||||
hull
|
||||
|
||||
proc ringTileOffTravel*(m: TFILRingModule, col, row: int,
|
||||
sx, sy, travelDeg: float): float =
|
||||
## Signed angle in degrees from the travel direction to the tile centre,
|
||||
## folded into (-180, 180]. Verbatim from `the_floor_is_lava.nim`'s
|
||||
## `tileOffTravel`; the ring's `ScoredTile` carries no `turnDeg`, so the
|
||||
## no-reversal pool computes the offsets itself.
|
||||
let tx = m.marginX + (col.float + 0.5) * GridSize
|
||||
let ty = m.marginY + (row.float + 0.5) * GridSize
|
||||
result = arctan2(ty - sy, tx - sx) * 180.0 / PI - travelDeg
|
||||
while result > 180.0: result -= 360.0
|
||||
while result < -180.0: result += 360.0
|
||||
|
||||
proc norevPool*(offs: openArray[float], threshold: float): seq[int] =
|
||||
## j165: which candidate tiles may a slow, mid-flight switch take? Verbatim
|
||||
## from `the_floor_is_lava.nim`. `offs` are the signed angles (deg) from the
|
||||
## travel direction to each candidate, `threshold` is the speed gate
|
||||
## (px/tick). Returns the indices NOT more than 90 deg off — the bot does not
|
||||
## have to turn around to reach them. If EVERY candidate is behind us the
|
||||
## reversal is unavoidable, so the single LEAST-bad one is returned (a shallow
|
||||
## turn, not a 180 deg flip): the result is NEVER empty, so the pick can never
|
||||
## be starved. `threshold <= 0` = the knob is off and every candidate stays.
|
||||
if offs.len == 0: return
|
||||
if threshold <= 0.0:
|
||||
for i in 0..<offs.len: result.add i
|
||||
return
|
||||
var keep: seq[int]
|
||||
for i, a in offs:
|
||||
if abs(a) <= 90.0: keep.add i
|
||||
if keep.len > 0: return keep
|
||||
var best = 0
|
||||
for i, a in offs:
|
||||
if abs(a) < abs(offs[best]): best = i
|
||||
@[best]
|
||||
|
||||
proc computeMove*(m: var TFILRingModule, ws: WorldState): MoveCommand =
|
||||
if m.cols == 0:
|
||||
m.initGrid(ws.arenaWidth, ws.arenaHeight)
|
||||
@@ -482,7 +556,13 @@ proc computeMove*(m: var TFILRingModule, ws: WorldState): MoveCommand =
|
||||
m.fire.prevEnergySet(ei.id, ei.energy)
|
||||
|
||||
# Tile-change replan: catches gradual displacement that position threshold misses
|
||||
if (not jumped) and (m.callCount > 0) and (m.commitTicks > 0):
|
||||
# j165: with TR_TFIL_RING_COMMIT_ARRIVAL the SELF-tile crossing is exactly the
|
||||
# event that must NOT cancel a commitment: crossing a boundary is the very
|
||||
# motion the commitment commands, and at GridSize 36 / speed 8 it fires every
|
||||
# ~5 ticks — which is precisely this fork's CommitTicks. Under the shipped
|
||||
# default (arrival off) this is the original block verbatim.
|
||||
if (not jumped) and (m.callCount > 0) and (m.commitTicks > 0) and
|
||||
not TfilRingCommitArrival:
|
||||
let curTileCol = clamp(int((ws.selfX - m.marginX) / GridSize), 0, m.cols - 1)
|
||||
let curTileRow = clamp(int((ws.selfY - m.marginY) / GridSize), 0, m.rows - 1)
|
||||
if curTileCol != m.lastTileCol or curTileRow != m.lastTileRow:
|
||||
@@ -756,22 +836,50 @@ proc computeMove*(m: var TFILRingModule, ws: WorldState): MoveCommand =
|
||||
safeTiles.add blockedTiles[i]
|
||||
blockedTiles = blockedTiles[promote ..< blockedTiles.len]
|
||||
|
||||
# Commitment logic
|
||||
# Commitment logic. With every j165 knob at its default (both off) this is the
|
||||
# original three-way test, unchanged. j165 adds one way OUT of a commitment
|
||||
# that is NOT a tile crossing (the block above is skipped when armed) and turns
|
||||
# the tick counter into a MINIMUM dwell: the target is held until we are
|
||||
# actually standing on it.
|
||||
let atTarget = (ws.selfX - m.commitTarget.x)^2 + (ws.selfY - m.commitTarget.y)^2 <
|
||||
RingArriveRadius * RingArriveRadius
|
||||
if m.commitTicks > 0:
|
||||
# Only allow danger replan after MinCommitTicks have elapsed
|
||||
inc m.commitAge
|
||||
# Only allow a replan after MinCommitTicks have elapsed
|
||||
let ticksElapsed = CommitTicks - m.commitTicks
|
||||
if ticksElapsed >= MinCommitTicks:
|
||||
let (cc, cr) = m.tileAt(m.commitTarget.x, m.commitTarget.y)
|
||||
let curLava = m.lavaAt(cc, cr)
|
||||
var commitEnd = false
|
||||
if curLava > m.commitLava + DangerReplanThreshold:
|
||||
# Mark committed tile blocked so we don't re-pick it
|
||||
# GENUINE DANGER: the committed tile got hot. Block it so we don't
|
||||
# immediately re-pick it, and replan. This safety valve is deliberately
|
||||
# independent of the arrival rule and is UNCHANGED by j165.
|
||||
m.blockedTile = (col: cc, row: cr, active: true)
|
||||
m.commitTicks = 0 # replan
|
||||
else:
|
||||
commitEnd = true
|
||||
elif TfilRingCommitArrival:
|
||||
if atTarget:
|
||||
# Reached. Only now is a new target allowed.
|
||||
commitEnd = true
|
||||
elif m.commitAge >= RingHullTicks:
|
||||
# Stall escape: past the planner's own reachability horizon the
|
||||
# committed tile is no longer guaranteed reachable (rammed, boxed in).
|
||||
commitEnd = true
|
||||
if not commitEnd:
|
||||
dec m.commitTicks
|
||||
if m.commitTicks == 0 and TfilRingCommitArrival:
|
||||
m.commitTicks = CommitTicks # minimum dwell reached: renew, don't abandon
|
||||
else:
|
||||
m.commitTicks = 0
|
||||
else:
|
||||
dec m.commitTicks
|
||||
|
||||
# j165: was the commitment we are about to replace still UNREACHED? A pick
|
||||
# that replaces a target we had not yet got to is the owner's failure mode:
|
||||
# the bot is still accelerating and the target flips under it. `picks == 0`
|
||||
# means this 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:
|
||||
# Filter out the blocked tile from candidates
|
||||
var candidates: seq[ScoredTile]
|
||||
@@ -780,6 +888,22 @@ proc computeMove*(m: var TFILRingModule, ws: WorldState): MoveCommand =
|
||||
continue
|
||||
candidates.add t
|
||||
if candidates.len == 0: candidates = safeTiles # all blocked → ignore block
|
||||
# j165, no opposite-direction flip while still accelerating. Below the speed
|
||||
# threshold the bot physically cannot complete a reversal before the bullet
|
||||
# lands, so a mid-flight switch to the mirror side only destroys the dodge it
|
||||
# already has. It is refused outright — and only for a MID-FLIGHT switch: if
|
||||
# we are already standing on the committed tile (an arrival pick) the bot is
|
||||
# free to go anywhere, and that is exactly the pick that must not be blocked.
|
||||
if TfilRingNoRevSpeed > 0.0 and abs(ws.selfSpeed) < TfilRingNoRevSpeed and midFlight:
|
||||
let travelDeg = if ws.selfSpeed < -0.01: ws.selfHeading + 180.0
|
||||
else: ws.selfHeading
|
||||
var offs: seq[float]
|
||||
for t in candidates:
|
||||
offs.add ringTileOffTravel(m, t.col, t.row, ws.selfX, ws.selfY, travelDeg)
|
||||
let keep = norevPool(offs, TfilRingNoRevSpeed)
|
||||
var narrowed: seq[ScoredTile]
|
||||
for i in keep: narrowed.add candidates[i]
|
||||
candidates = narrowed
|
||||
# Safety is a HARD constraint: the weighting below only re-orders the draw
|
||||
# AMONG `candidates`, which is exactly the pool the old `rand` picked from.
|
||||
# It can never select a tile the unweighted code would have rejected
|
||||
@@ -801,6 +925,8 @@ proc computeMove*(m: var TFILRingModule, ws: WorldState): MoveCommand =
|
||||
y: m.marginY + (ct.row.float + 0.5) * GridSize)
|
||||
m.commitTicks = CommitTicks
|
||||
m.commitLava = m.lavaAt(ct.col, ct.row)
|
||||
m.commitAge = 0
|
||||
inc m.picks
|
||||
m.blockedTile.active = false # clear after successful pick
|
||||
|
||||
# One concise log line on a range-class or band change (never per-tick).
|
||||
|
||||
Reference in New Issue
Block a user