TMComposites gate: per-gun confidence faithful for 3 guns; no pair composes

Adds a per-sample intrinsic-confidence field (GunPrediction.confidence,
threaded through FeedbackEvent/VirtualBullet, populated by Pattern, DecayGF,
KNN, GuessFactor, Tsetlin, TMHorizon) and an offline recorder + analyzer that
reproduce the paper's Figure 2 per gun and its Eq-8 composite.

Measured on 3 held-out tr-bridge DrussGT battles (33k ticks, ~133k samples/gun):
- FAITHFUL: DecayGF (rho +0.133), KNN (+0.090), Pattern (+0.064, weak).
- GuessFactor is ANTI-faithful (rho -0.067); Tsetlin c_max is useless (0.001).
- No pair of guns specialises complementarily: the same gun dominates both
  high-confidence slices in every pair.
- Eq-8 alpha-normalised confidence-weighted composite: 18.41% vs Pattern
  20.45% (McNemar p=3.1e-126). Faithful-only variant 18.68%, still loses.
  Shuffle control passes weakly (composite > shuffle, p=4e-14) so ~0.7pp of
  competence is real but ~2pp short. Offline veto: design is dead.

See docs/tmcomposites_gate.md.
This commit is contained in:
2026-09-25 22:02:28 +02:00
parent d0750ab020
commit f58d65d2e8
12 changed files with 2666 additions and 11 deletions
+13 -3
View File
@@ -50,6 +50,10 @@ type
cacheValid: bool
cacheTick: int
bestMatch: int ## -1 = no usable match (linear fallback)
lastMatchScore*: float ## best pattern-match cost (lower = better);
## set by findBestMatch, exposed as the gun's
## intrinsic per-sample confidence
## (TMComposites gate, docs/tmcomposites_gate.md)
playStart: int
playAvail: int
pathX: array[HistorySize + 1, float]
@@ -87,9 +91,11 @@ proc linearPredict(state: WorldState, bulletSpeed: float): (float, float) =
# --- pattern search + play-forward ---
proc findBestMatch(g: PatternMatcherGun): int =
proc findBestMatch(g: var PatternMatcherGun): int =
## Speed-independent history search. Returns the start index of the best
## matching pattern, or -1 when there is not enough history.
## matching pattern, or -1 when there is not enough history. Stores the best
## match cost in `g.lastMatchScore` for the confidence readout.
g.lastMatchScore = Inf
if g.count < PatternLen * 2:
return -1
@@ -111,6 +117,7 @@ proc findBestMatch(g: PatternMatcherGun): int =
if score < bestScore:
bestScore = score
bestMatch = i
g.lastMatchScore = bestScore
bestMatch
proc buildPath(g: var PatternMatcherGun, state: WorldState, bestMatch: int) =
@@ -222,7 +229,10 @@ proc predict*(g: var PatternMatcherGun, state: WorldState,
return g.applyRadial(state, px, py)
let (px, py) = g.projectFromPath(state, bulletSpeed)
g.applyRadial(state, px, py)
result = g.applyRadial(state, px, py)
# Match quality as a confidence: a perfect historical match (cost 0) gives 1.0,
# a worse match decays toward 0. Deterministic and per-sample.
result.confidence = 1.0 / (1.0 + max(0.0, g.lastMatchScore))
proc onResult*(g: var PatternMatcherGun, e: FeedbackEvent) =
discard # pattern matcher learns from movement observation, not feedback