j127: forced-share allocator (TR_RACK_SHARE, default-off) + allocation batch pre-registration
This commit is contained in:
@@ -135,6 +135,105 @@ proc rackActive*(membership: openArray[RackMembership], mode: RackMode): string
|
||||
result.add RackGunNames[i]
|
||||
if result.len == 0: result = "FULL"
|
||||
|
||||
# ── forced-share allocator (TR_RACK_SHARE) ───────────────────────────────────
|
||||
#
|
||||
# The shipped selector RANKS guns and lets the ranking (plus hysteresis) decide
|
||||
# each gun's share. `GUN_SELECTOR_FLOOR` is a FITNESS floor, so nothing
|
||||
# guarantees the second gun ANY share of the shots — the ~66/34 split observed
|
||||
# with a 2-gun rack is an OUTCOME, not a policy. `TR_RACK_SHARE` makes the share
|
||||
# a POLICY: when set, the live selection is a deterministic deficit-round-robin
|
||||
# over the named, ADMITTED guns instead of `chooseFromFit`'s ranking.
|
||||
#
|
||||
# TR_RACK_SHARE=pattern:50,bitbrain:50
|
||||
# TR_RACK_SHARE=pattern:0.7,bitbrain:0.3
|
||||
#
|
||||
# Values are RELATIVE weights (fractions or percentages — only the ratio
|
||||
# matters) and names are case-insensitive `RackGunNames` (the `GunNames` rack
|
||||
# order). The schedule holds each allocated gun for the selector dwell window
|
||||
# (`GUN_SELECTOR_DWELL`) so the turret converges between switches, exactly like
|
||||
# the shipped hysteresis; the share is therefore over dwell EPOCHS, and selected
|
||||
# ticks follow the weight ratio (the `gun_stats.jsonl` `selected` counts are the
|
||||
# liveness proof). OFF by default: an unset variable leaves the weights empty
|
||||
# and `selectGun` takes the unchanged ranking path byte-for-byte.
|
||||
|
||||
const RackShareEnvVar* = "TR_RACK_SHARE"
|
||||
|
||||
type
|
||||
RackShare* = object
|
||||
active*: bool
|
||||
weights*: seq[float] ## indexed by gun id; 0.0 = not in the schedule
|
||||
named*: seq[string] ## gun names in declared order (audit only)
|
||||
|
||||
proc parseRackShare*(value: string,
|
||||
membership: openArray[RackMembership]): RackShare =
|
||||
## Parse `TR_RACK_SHARE`. Empty / malformed input is never fatal: it warns on
|
||||
## stderr and returns an INACTIVE share (empty weights), so a typo can only
|
||||
## fall back to the shipped selector, never take the bot down. A named gun
|
||||
## that the rack removes (`TR_RACK_<GUN>=off`) is a loud ERROR and also leaves
|
||||
## the share inactive — a forced share must only allocate among ADMITTED guns.
|
||||
let v = value.strip()
|
||||
if v.len == 0: return
|
||||
var weights = newSeq[float](len(RackGunNames))
|
||||
var named: seq[string]
|
||||
var total = 0.0
|
||||
for part in v.split(','):
|
||||
let p = part.strip()
|
||||
if p.len == 0: continue
|
||||
let ci = p.find(':')
|
||||
if ci <= 0 or ci == p.high:
|
||||
stderr.writeLine("[gun_harness] ERROR: bad " & RackShareEnvVar &
|
||||
" entry '" & p & "' (want GUN:weight); share disabled")
|
||||
return
|
||||
let gname = p[0..<ci].strip()
|
||||
var wstr = p[ci+1..^1].strip()
|
||||
if wstr.endsWith("%"): wstr = wstr[0..^2].strip()
|
||||
var w: float
|
||||
try: w = parseFloat(wstr)
|
||||
except ValueError:
|
||||
stderr.writeLine("[gun_harness] ERROR: bad " & RackShareEnvVar &
|
||||
" weight '" & wstr & "' for '" & gname &
|
||||
"'; share disabled")
|
||||
return
|
||||
if w < 0.0 or w != w:
|
||||
stderr.writeLine("[gun_harness] ERROR: " & RackShareEnvVar &
|
||||
" weight for '" & gname & "' must be >= 0; share disabled")
|
||||
return
|
||||
var gid = -1
|
||||
for i in 0..<len(RackGunNames):
|
||||
if RackGunNames[i].toLowerAscii() == gname.toLowerAscii():
|
||||
gid = i
|
||||
break
|
||||
if gid < 0:
|
||||
stderr.writeLine("[gun_harness] ERROR: unknown gun '" & gname & "' in " &
|
||||
RackShareEnvVar & " (valid: " &
|
||||
RackGunNames.join("|") & "); share disabled")
|
||||
return
|
||||
if gid < membership.len and membership[gid] == rmOff:
|
||||
stderr.writeLine("[gun_harness] ERROR: " & RackShareEnvVar & " names '" &
|
||||
RackGunNames[gid] & "' but TR_RACK_" & RackGunNames[gid] &
|
||||
"=off — a forced share allocates only among ADMITTED " &
|
||||
"guns; share disabled")
|
||||
return
|
||||
weights[gid] += w
|
||||
named.add RackGunNames[gid]
|
||||
total += w
|
||||
if total <= 0.0:
|
||||
stderr.writeLine("[gun_harness] ERROR: " & RackShareEnvVar &
|
||||
" weights sum to <= 0; share disabled")
|
||||
return
|
||||
for i in 0..<weights.len: weights[i] /= total
|
||||
result = RackShare(active: true, weights: weights, named: named)
|
||||
var parts: seq[string]
|
||||
for i in 0..<weights.len:
|
||||
if weights[i] > 0.0: parts.add RackGunNames[i] & "=" & $weights[i]
|
||||
stderr.writeLine("[gun_harness] " & RackShareEnvVar & " active: " &
|
||||
parts.join(" ") & " (deficit round-robin over dwell epochs)")
|
||||
|
||||
let ActiveRackShare* = parseRackShare(getEnv(RackShareEnvVar, ""),
|
||||
ActiveRackMembership)
|
||||
## Process-wide forced-share schedule, frozen at startup. Empty weights when
|
||||
## `TR_RACK_SHARE` is unset or malformed.
|
||||
|
||||
const
|
||||
## ── Range-aware firing gate ────────────────────────────────────────────────
|
||||
## A real shot departs with whatever misalignment the gun had at fire time,
|
||||
@@ -200,7 +299,8 @@ proc selectShotPolicy*(t: var VirtualTracker, targetId = -1, tick = 0,
|
||||
enemyEnergy = 100.0,
|
||||
ramming = false,
|
||||
rackMode: RackMode = rm1v1,
|
||||
membership: openArray[RackMembership] = []
|
||||
membership: openArray[RackMembership] = [],
|
||||
share: seq[float] = ActiveRackShare.weights
|
||||
): (GunId, int, float, PowerCap) =
|
||||
## `selectShot` plus the energy-aware power-policy decision, so a caller can
|
||||
## log the cap and its reason (see `applyPowerPolicy` in virtual_bullets).
|
||||
@@ -215,7 +315,8 @@ proc selectShotPolicy*(t: var VirtualTracker, targetId = -1, tick = 0,
|
||||
## is the process-wide `TR_RACK_*` table, passed by the live bot. An empty
|
||||
## membership admits every gun (the pre-change behaviour).
|
||||
let gunId = t.selectGun(targetId, tick,
|
||||
rackMode = rackMode, membership = membership)
|
||||
rackMode = rackMode, membership = membership,
|
||||
share = share)
|
||||
let (prefBin, preferred) = t.bestPower(gunId, targetId)
|
||||
# pEst / pRef mirror `bestPower`'s own fitness source (per-target when data
|
||||
# exists, else the deterministic aggregate). An empty bin carries no rate of
|
||||
|
||||
Reference in New Issue
Block a user