diff --git a/common_libs/gun_harness/selector.nim b/common_libs/gun_harness/selector.nim index 3f23489..c784948 100644 --- a/common_libs/gun_harness/selector.nim +++ b/common_libs/gun_harness/selector.nim @@ -15,17 +15,33 @@ import virtual_bullets # GUN_RACK_DISABLE. A gun's membership admits it into the 1v1 rack, the melee # rack, both, or neither: # -# TR_RACK_HEADON=both (shipped default for every gun) +# TR_RACK_PATTERN=both (shipped default: the ONLY admitted gun) +# TR_RACK_HEADON=both -> re-admit HeadOn (used to restore the old rack) # TR_RACK_TSETLIN=1v1 -> 1v1 rack only # TR_RACK_DISPLACE=melee -> melee rack only # TR_RACK_KNN=off -> removed from both racks # TR_RACK_TMPATTERN=off (shipped default for the new TM pattern gun) # +# SHIPPED DEFAULT IS `onlyPattern`: Pattern (id 5) is admitted in both racks and +# every other gun is `off`. This is a deliberate, measured decision, not a +# pruning heuristic — the virtual-fitness selector was measured to be NEGATIVE +# value at every rack size tested (full, lean8, lean6, pairPC/PK/PL) and against +# 10/10 adversaries, while Pattern alone is the best single gun in general. See +# docs/selector_negative_value.md. The selector MECHANISM is retained in full +# (chooseFromFit, the floor/band logic, hysteresis, virtual fitness) — the rack +# merely has one member by default, so re-enabling any gun is a one-line env +# override with no rebuild: +# +# Revert to the old full rack (all guns `both`, TMPATTERN `off`): +# TR_RACK_PATTERN=both TR_RACK_HEADON=both TR_RACK_LINEAR=both \ +# TR_RACK_TSETLIN=both TR_RACK_CIRCULAR=both TR_RACK_GUESSFACTOR=both \ +# TR_RACK_WALLBOUNCE=both TR_RACK_ACCEL=both TR_RACK_STOPSHOT=both \ +# TR_RACK_DISPLACE=both TR_RACK_AVGLEAD=both TR_RACK_DECAYGF=both \ +# TR_RACK_KNN=both TR_RACK_TMSELECT=both ./ModularBot +# # The mode itself is derived from SERVER truth (`getEnemyCount()`), never from # the tracker's known-enemy count, by `rackMode` in virtual_bullets — the same -# transition the radar uses. Every gun except TMPATTERN defaults to `both`, so an -# unset environment preserves the pre-change single-rack selection byte-for-byte; -# TMPATTERN defaults to `off` so it cannot alter that selection. +# transition the radar uses. const RackGunNames*: array[15, string] = [ @@ -33,16 +49,30 @@ const "WALLBOUNCE", "ACCEL", "STOPSHOT", "DISPLACE", "AVGLEAD", "DECAYGF", "KNN", "TMSELECT", "TMPATTERN"] RackEnvPrefix* = "TR_RACK_" - ## Defaults are all-`both` EXCEPT the new TM pattern gun (id 14), which ships - ## `off`: it is registered and forceable (`TR_RACK_TMPATTERN=both|1v1|melee`) - ## but never spawns a virtual bullet unless explicitly enabled, so the shared - ## VirtualTracker ring head — and therefore every other gun's learning order - ## and the default selection sequence — is byte-for-byte unchanged. Defaulting - ## it to `both` would let it compete for selection and change the default rack. + ## SHIPPED DEFAULT: `onlyPattern`. Pattern (id 5) is admitted in both racks; + ## every other gun is `off`. The selection mechanism is untouched and remains + ## fully functional — only the rack's membership changed. Re-enable any gun + ## with `TR_RACK_`, or restore the old full rack with the one-liner in the + ## header comment. TMPATTERN (id 14) stays `off`: registered and forceable but + ## it never spawns a virtual bullet unless explicitly enabled, so the shared + ## VirtualTracker ring head — and every other gun's learning order — is + ## unchanged. DefaultRackMembership*: array[15, RackMembership] = [ - rmBoth, rmBoth, rmBoth, rmBoth, rmBoth, rmBoth, rmBoth, - rmBoth, rmBoth, rmBoth, rmBoth, rmBoth, rmBoth, rmBoth, - rmOff] + rmOff, # 0 HEADON — off (measured: worst over-selected gun) + rmOff, # 1 LINEAR — off + rmOff, # 2 TSETLIN — off + rmOff, # 3 CIRCULAR — off + rmOff, # 4 GUESSFACTOR — off + rmBoth, # 5 PATTERN — the only admitted gun (best single gun in general) + rmOff, # 6 WALLBOUNCE — off + rmOff, # 7 ACCEL — off + rmOff, # 8 STOPSHOT — off + rmOff, # 9 DISPLACE — off + rmOff, # 10 AVGLEAD — off + rmOff, # 11 DECAYGF — off + rmOff, # 12 KNN — off + rmOff, # 13 TMSELECT — off + rmOff] # 14 TMPATTERN — off (already shipped off; TM pattern gun) proc parseRackMembership*(value: string): RackMembership = ## Parse a `TR_RACK_` value. Empty / unknown values fall back to the @@ -85,7 +115,7 @@ proc rackModeName*(m: RackMode): string = proc rackOverrides*(membership: openArray[RackMembership]): string = ## Compact `GUN:mode,GUN:mode` list of entries that differ from the shipped - ## all-`both` default. Empty when the rack is at its default. + ## default table. Empty when the rack is at its default. for i in 0.. 0: result.add "," diff --git a/common_libs/tests/test_rack_membership.nim b/common_libs/tests/test_rack_membership.nim index f610c0a..95ea0de 100644 --- a/common_libs/tests/test_rack_membership.nim +++ b/common_libs/tests/test_rack_membership.nim @@ -5,8 +5,9 @@ ## * mode filtering selects the right gun subset, ## * an EMPTY filtered set falls back to the full rack, ## * `TR_RACK_` env overrides parse correctly (both / 1v1 / melee / off), -## * the shipped defaults are all-`both`, -## * the default configuration reproduces the pre-change selection exactly. +## * the shipped default is `onlyPattern` (Pattern admitted, all else `off`), +## * the revert override restores the old full rack, +## * an all-`both` membership reproduces the pre-change selection exactly. ## ## Run with plain: ## nim c -r common_libs/tests/test_rack_membership.nim @@ -137,21 +138,84 @@ proc testParseMembership() = check "parse: unknown -> rmBoth (never fatal)", parseRackMembership("banana") == rmBoth -proc testDefaultsAllBoth() = +proc testDefaultsOnlyPattern() = clearRackEnv() - # The shipped table is all-`both` EXCEPT the new TM pattern gun (id 14), - # which defaults to `off` so it cannot alter the default selection. - var allBoth = true + # SHIPPED DEFAULT: `onlyPattern`. Pattern (id 5) is `both`; every other gun is + # `off`. The selector mechanism is untouched, but the rack has one member. + const PatternId = 5 + var exactlyOne = true + var admittedCount = 0 for i in 0.. floor. + seedWindow(t, 7, 0, 0, 50, 0) + var diag: SelectorDiag + let g = t.bestGun(7, addr diag, rackMode = rm1v1, + membership = DefaultRackMembership) + check "floor: fires when the only admitted gun has no data", diag.floorFired + check "floor: returns the ADMITTED gun (Pattern, id 5), NOT HeadOn", g == 5 + # Same field, same floor, but with HeadOn admissible: it returns gun 0, which + # is exactly WHY HeadOn had to be removed for the floor to be safe. + var oldAll: array[15, RackMembership] + for i in 0..<15: oldAll[i] = rmBoth + var t2 = initTracker(15) + seedWindow(t2, 7, 0, 0, 0, 50) # HeadOn 0%, eligible (>= MinObs) + var d2: SelectorDiag + let gOld = t2.bestGun(7, addr d2, rackMode = rm1v1, membership = oldAll) + check "floor: with HeadOn admitted the old floor returns gun 0", + d2.floorFired and gOld == 0 + # The LIVE path (`selectGun`) takes the same floor branch. + var t3 = initTracker(15) + seedWindow(t3, 7, 0, 0, 50, 0) + var d3: SelectorDiag + let gLive = t3.selectGun(7, tick = 0, diag = addr d3, rackMode = rm1v1, + membership = DefaultRackMembership) + check "floor: the live selectGun floor also returns Pattern", + d3.floorFired and gLive == 5 + +proc testRevertOverrideRestoresFullRack() = + ## The documented one-liner: every TR_RACK_ back to `both` (TMPATTERN + ## stays `off`). This must reproduce the old full rack without a rebuild. + clearRackEnv() + putEnv("TR_RACK_PATTERN", "both") + for name in RackGunNames: + if name != "TMPATTERN": putEnv("TR_RACK_" & name, "both") + let m = loadRackMembership() + var full = true + for i in 0.. **This contradicts the user's standing directive to keep virtual-fitness +> selection — stated plainly.** The mechanism is retained and remains +> trivially re-enableable; the **default no longer uses it**, because the +> measurement says the selector is negative value on every rack size tested +> (full, lean8, lean6, pairPC/PK/PL) and on **10/10 adversaries** (significant +> wins for Pattern-alone on 4, ties on 6, zero significant losses). +> +> **Honesty caveat (unchanged):** 4 of the 10 opponents (SpinBot, Corners, +> Crazy, RamFire) are **Tank Royale sample-bot PORTS**, not the original classic +> jars; **DrussGT is the only real classic jar**, hosted through the +> `robocode_shim` bridge. The strongest evidence — the significant wins on +> DrussGT (+3.11 pp, p=0.0023), Corners, Crazy and PatternMover — includes the +> one real classic jar, but the port substitution is not hidden. See +> [Adversaries actually available](#adversaries-actually-available-what-is-a-real-classic-opponent-and-what-is-not). **Measured. The best single gun beats the full rack, and not by a little.** @@ -193,11 +229,18 @@ The selected-gun mix (liveness proof) shows the virtual signal still over-select The failure is not "the rack is too big". It is that the **virtual fitness signal ranks the wrong guns first**, on a big rack and on a small one alike. -### Ship recommendation +### Ship recommendation — SHIPPED **`onlyPattern`** — `Pattern` alone, selector bypassed. Measured: **10.36% real hit rate, 264 damage/run** (vs `lean8` 6.31%/146 and `full` 6.93%/159). This replicates the original finding (10.78%/287). +**This recommendation is now the shipped default** (see the banner at the top of +this document). `onlyPattern` was previously achieved with the rack knobs +(`TR_RACK_=off` for every gun but Pattern); as of this change it is the +unset-environment default, and the same knobs restore any other rack. The +selector mechanism is unchanged, so the A/B that produced the table above still +reproduces byte-for-byte with the revert one-liner. + Method note: one frozen binary built from clean `e0666a5` via `git archive` (other agents had `common_libs/guns/*` dirty and have since committed further changes; this build predates them), sha256 `df8d4f2e…`. 6 arms × 7 runs × 7 rounds, 8 concurrent bridge battles. Liveness verified for every arm from