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:
@@ -42,6 +42,10 @@ import std/strutils except fromHex # `fromHex` would clash with color.fromHex
|
||||
import gun_harness/gun_interface
|
||||
# Private-field access: include (do NOT import) the shipped mover.
|
||||
include movements/the_floor_is_lava
|
||||
# j165: the TFIL-RING fork's arrival commitment. `tfil_ring_replay` includes
|
||||
# the ring mover (for its PRIVATE commitTarget/commitTicks) and re-exports it,
|
||||
# so this is the only ring import the guard needs.
|
||||
import tfil_ring_replay
|
||||
|
||||
const repoRoot = currentSourcePath().parentDir.parentDir.parentDir
|
||||
const fixtureRel = "tr_drussgt_vs_modularbot.jsonl"
|
||||
@@ -1479,6 +1483,130 @@ proc testJ154() =
|
||||
check "j154: clearing the knob restores today's behaviour exactly",
|
||||
TfilHoldMaxTicks == 0
|
||||
|
||||
# ── j165: the TFIL-RING arrival commitment (TR_TFIL_RING_*, default OFF) ─────
|
||||
# The same two mechanisms ported from `the_floor_is_lava.nim` (j144) onto
|
||||
# RING-SPECIFIC env names, so the two forks never share a namespace by
|
||||
# accident. What must hold, and nothing more:
|
||||
# 1. DEFAULT PARITY: with both knobs unset the ring mover is byte-for-byte
|
||||
# the PRE-CHANGE ring mover over the whole fixture replay.
|
||||
# 2. the arrival commitment ENGAGES: the committed target is actually
|
||||
# REACHED far more often, and there are strictly fewer mid-flight
|
||||
# re-targets than the shipped fixed 5-tick dwell.
|
||||
# 3. the no-reversal pool is SPEED-GATED: the stream may only diverge from
|
||||
# the unarmed build on a tick whose |selfSpeed| is below the gate, and the
|
||||
# pool itself can never be emptied.
|
||||
when declared(TfilRingCommitArrival):
|
||||
|
||||
proc ringRunStats(): tuple[picks, reached: int] =
|
||||
## Count picks over the replay and how many of them REPLACED a target the bot
|
||||
## had actually stood on (< 18px, the same arrival radius the mover uses).
|
||||
## Under a fixed dwell this is rare: the target is replaced mid-flight.
|
||||
randomize(Seed)
|
||||
var m = initTFILRing()
|
||||
let states = loadStates()
|
||||
let starts = loadRoundStarts()
|
||||
var prev = (x: 0.0, y: 0.0)
|
||||
var hadPick = false
|
||||
for i in 0..<states.len:
|
||||
if i == 0 or i in starts:
|
||||
m.resetRound()
|
||||
hadPick = false
|
||||
let ws = states[i]
|
||||
let before = ringPicks(m)
|
||||
discard m.computeMove(ws)
|
||||
if ringPicks(m) != before:
|
||||
inc result.picks
|
||||
if hadPick and
|
||||
sqrt((ws.selfX - prev.x)^2 + (ws.selfY - prev.y)^2) < RingArriveRadius:
|
||||
inc result.reached
|
||||
prev = ringTarget(m)
|
||||
hadPick = true
|
||||
|
||||
proc testJ165() =
|
||||
doAssert fileExists(RingGoldenPath), "missing golden: " & RingGoldenPath
|
||||
let recs = replayRing() # both knobs unset; the shipped fire detector, as
|
||||
# the golden was generated
|
||||
var golden: seq[string]
|
||||
for rawLine in lines(RingGoldenPath):
|
||||
if rawLine.startsWith("#"): continue
|
||||
let line = rawLine.strip()
|
||||
if line.len > 0: golden.add line
|
||||
check "j165: ring golden covers the whole fixture (>= 15000 ticks)",
|
||||
golden.len >= 15000
|
||||
check "j165: the unset replay covers the same number of ticks",
|
||||
recs.len == golden.len
|
||||
var firstDiff = -1
|
||||
for i in 0..<min(recs.len, golden.len):
|
||||
if ringRecLine(recs[i]) != golden[i]:
|
||||
firstDiff = i
|
||||
break
|
||||
check "j165: BOTH KNOBS UNSET IS BYTE-FOR-BYTE THE PRE-CHANGE RING MOVER " &
|
||||
"(speed/turnRate/target/commitTicks) over " & $recs.len & " ticks",
|
||||
firstDiff < 0
|
||||
if firstDiff >= 0:
|
||||
echo " first divergence at tick index ", firstDiff, ": got [",
|
||||
ringRecLine(recs[firstDiff]), "] want [", golden[firstDiff], "]"
|
||||
|
||||
delEnv("TR_TFIL_RING_COMMIT_ARRIVAL")
|
||||
delEnv("TR_TFIL_RING_NOREV_SPEED")
|
||||
loadTfilRingCommitEnv()
|
||||
check "j165: both knobs DEFAULT OFF with the env deleted",
|
||||
(not TfilRingCommitArrival) and TfilRingNoRevSpeed == 0.0
|
||||
putEnv("TR_TFIL_COMMIT_ARRIVAL", "1") # tfil's names must NOT leak across
|
||||
putEnv("TR_TFIL_NOREV_SPEED", "9")
|
||||
loadTfilRingCommitEnv()
|
||||
check "j165: the env names are RING-SPECIFIC — tfil's " &
|
||||
"TR_TFIL_COMMIT_ARRIVAL / TR_TFIL_NOREV_SPEED leave ring untouched",
|
||||
(not TfilRingCommitArrival) and TfilRingNoRevSpeed == 0.0
|
||||
delEnv("TR_TFIL_COMMIT_ARRIVAL")
|
||||
delEnv("TR_TFIL_NOREV_SPEED")
|
||||
|
||||
# 2. the arrival commitment engages
|
||||
let off = ringRunStats()
|
||||
TfilRingCommitArrival = true
|
||||
let on = ringRunStats()
|
||||
TfilRingCommitArrival = false
|
||||
check "j165: ARRIVAL ENGAGES — a committed target is actually REACHED on " &
|
||||
$on.reached & "/" & $on.picks & " picks, vs " & $off.reached & "/" &
|
||||
$off.picks & " on the shipped fixed 5-tick dwell",
|
||||
on.picks > 0 and (on.reached.float / on.picks.float) >
|
||||
(off.reached.float / off.picks.float)
|
||||
check "j165: ... and it holds instead of re-targeting mid-flight: " &
|
||||
$on.picks & " picks vs " & $off.picks & " on the same replay",
|
||||
on.picks < off.picks
|
||||
|
||||
# 3. the no-reversal pool is speed-gated, and can never be emptied
|
||||
check "j165: norevPool with the gate off returns EVERY candidate",
|
||||
norevPool(@[10.0, 120.0, -170.0], 0.0) == @[0, 1, 2]
|
||||
check "j165: norevPool armed keeps only the non-reversing candidates",
|
||||
norevPool(@[10.0, 120.0, -170.0], 4.0) == @[0]
|
||||
check "j165: norevPool never empties — all-behind falls back to the least bad",
|
||||
norevPool(@[170.0, 150.0, 179.0], 4.0) == @[1] and
|
||||
norevPool(@[170.0, 179.0], 4.0).len > 0
|
||||
# Engine gate: the streams can only DIVERGE at a gated pick. (Divergence
|
||||
# then persists for many ticks — a different target steers differently — so
|
||||
# "every diverging tick is slow" is the wrong claim; "the FIRST divergence
|
||||
# is a slow-tick pick" is the right one.)
|
||||
TfilRingNoRevSpeed = 4.0
|
||||
let armed = replayRing()
|
||||
TfilRingNoRevSpeed = 0.0
|
||||
var nDiv = 0
|
||||
var firstArmed = -1
|
||||
for i in 0..<min(recs.len, armed.len):
|
||||
if ringRecLine(recs[i]) != ringRecLine(armed[i]):
|
||||
inc nDiv
|
||||
if firstArmed < 0: firstArmed = i
|
||||
let states = loadRingStates()
|
||||
check "j165: the no-reversal treatment APPLIES (gate 4.0 changes " & $nDiv &
|
||||
" of " & $recs.len & " ticks — an A/B whose treatment never fires is worthless)",
|
||||
nDiv > 0
|
||||
check "j165: ... and it is SPEED-GATED: the first divergence (tick " &
|
||||
$firstArmed & ") is a PICK made at |selfSpeed| = " &
|
||||
$(if firstArmed >= 0: abs(states[firstArmed].selfSpeed) else: -1.0) &
|
||||
" < 4.0",
|
||||
firstArmed >= 0 and armed[firstArmed].picked and
|
||||
abs(states[firstArmed].selfSpeed) < 4.0
|
||||
|
||||
# ── driver ───────────────────────────────────────────────────────────────────
|
||||
|
||||
testDefaultParity()
|
||||
@@ -1494,6 +1622,8 @@ when declared(loadTfilCommitEnv):
|
||||
testJ150()
|
||||
testJ154()
|
||||
testJ153()
|
||||
when declared(TfilRingCommitArrival):
|
||||
testJ165()
|
||||
|
||||
if failures > 0:
|
||||
echo "\n", failures, " check(s) FAILED"
|
||||
|
||||
Reference in New Issue
Block a user