feat(ModularBot): 6 guns, pattern matcher, melee modules, adversarial bots

- New guns: guess-factor (GF histogram), pattern-matcher (movement tape replay)
- New modules: minimum-risk melee movement, spinning melee radar
- New test bots: PatternMover, RandomMover, WaveSurfer
- Fixed: FeedbackEvent now carries actualX/actualY for proper GF learning
- Fixed: TM gun warmup gating + directional residuals
- Fixed: circular gun integrated formula + multi-bin omega cache
- Fixed: oscillator wall-bounce lockout
- Fixed: phantom meteor perpendicular body orientation
- 6/6 battle wins across all enemy types
This commit is contained in:
2026-09-20 00:59:53 +02:00
parent 254c7dc997
commit 1ed7797cb6
184 changed files with 80149 additions and 21 deletions
+310
View File
@@ -0,0 +1,310 @@
"""
Battle data correlation analysis.
stdlib + csv + math only.
"""
import csv
import math
import sys
from collections import defaultdict
DATA = "/home/davide/Projects/SirRoboGarage/BNNBot_garage/data/battle_1_decimal.csv"
POWER_LEVELS = [0.10, 0.42, 0.74, 1.07, 1.39, 1.71, 2.03, 2.36, 2.68, 3.00]
FRAME_FIELDS = ["bearing_sin", "bearing_cos", "distance", "velocity",
"heading_sin", "heading_cos", "enemy_x", "enemy_y", "enemy_energy"]
def pname(p):
return f"p{p:.2f}"
def corr(xs, ys):
n = len(xs)
if n < 2:
return float("nan")
mx = sum(xs) / n
my = sum(ys) / n
num = sum((x - mx) * (y - my) for x, y in zip(xs, ys))
dx = math.sqrt(sum((x - mx) ** 2 for x in xs))
dy = math.sqrt(sum((y - my) ** 2 for y in ys))
if dx == 0 or dy == 0:
return float("nan")
return num / (dx * dy)
def stats(vals):
if not vals:
return dict(mean=float("nan"), std=float("nan"), mn=float("nan"), mx=float("nan"))
n = len(vals)
mean = sum(vals) / n
std = math.sqrt(sum((v - mean) ** 2 for v in vals) / n)
return dict(mean=mean, std=std, mn=min(vals), mx=max(vals))
def main():
with open(DATA) as f:
reader = csv.DictReader(f)
all_rows = list(reader)
# 1. Filter fully-resolved rows
output_cols = []
for p in POWER_LEVELS:
for field in FRAME_FIELDS:
output_cols.append(f"{pname(p)}_{field}")
rows = [r for r in all_rows if not any(r[c] == "NA" for c in output_cols if c in r)]
print(f"=== 1. FILTERING ===")
print(f"Total rows: {len(all_rows)}, Fully-resolved: {len(rows)}\n")
def fv(row, frame, field):
return float(row[f"f{frame}_{field}"])
def pv(row, p, field):
return float(row[f"{pname(p)}_{field}"])
# 2. Delta analysis
print("=== 2. DELTA ANALYSIS (hit_state - f0_state) ===")
for p in POWER_LEVELS:
dx_vals = [pv(r, p, "enemy_x") - fv(r, 0, "enemy_x") for r in rows]
dy_vals = [pv(r, p, "enemy_y") - fv(r, 0, "enemy_y") for r in rows]
dd_vals = [pv(r, p, "distance") - fv(r, 0, "distance") for r in rows]
dv_vals = [pv(r, p, "velocity") - fv(r, 0, "velocity") for r in rows]
dbs_vals = [pv(r, p, "bearing_sin") - fv(r, 0, "bearing_sin") for r in rows]
sx, sy, sd, sv, sbs = stats(dx_vals), stats(dy_vals), stats(dd_vals), stats(dv_vals), stats(dbs_vals)
print(f" power={p:.2f}:")
print(f" delta_x: mean={sx['mean']:+7.2f} std={sx['std']:6.2f} [{sx['mn']:+7.2f}, {sx['mx']:+7.2f}]")
print(f" delta_y: mean={sy['mean']:+7.2f} std={sy['std']:6.2f} [{sy['mn']:+7.2f}, {sy['mx']:+7.2f}]")
print(f" delta_dist: mean={sd['mean']:+7.2f} std={sd['std']:6.2f} [{sd['mn']:+7.2f}, {sd['mx']:+7.2f}]")
print(f" delta_vel: mean={sv['mean']:+7.2f} std={sv['std']:6.2f} [{sv['mn']:+7.2f}, {sv['mx']:+7.2f}]")
print(f" delta_bsin: mean={sbs['mean']:+7.4f} std={sbs['std']:.4f} [{sbs['mn']:+7.4f}, {sbs['mx']:+7.4f}]")
print()
# 3. Velocity → displacement correlation
print("=== 3. VELOCITY -> DISPLACEMENT CORRELATION ===")
for p in POWER_LEVELS:
vel = [fv(r, 0, "velocity") for r in rows]
dx = [pv(r, p, "enemy_x") - fv(r, 0, "enemy_x") for r in rows]
dy = [pv(r, p, "enemy_y") - fv(r, 0, "enemy_y") for r in rows]
# magnitude of displacement
disp = [math.sqrt(x**2 + y**2) for x, y in zip(dx, dy)]
r_vx = corr(vel, dx)
r_vy = corr(vel, dy)
r_vd = corr(vel, disp)
print(f" power={p:.2f}: corr(vel,dx)={r_vx:+.3f} corr(vel,dy)={r_vy:+.3f} corr(vel,|disp|)={r_vd:+.3f}")
print()
# 4. Frame-to-frame velocity (position deltas between consecutive frames)
print("=== 4. FRAME-TO-FRAME VELOCITY (position deltas) ===")
for fi in range(9):
vx_vals = [fv(r, fi, "enemy_x") - fv(r, fi+1, "enemy_x") for r in rows]
vy_vals = [fv(r, fi, "enemy_y") - fv(r, fi+1, "enemy_y") for r in rows]
sx, sy = stats(vx_vals), stats(vy_vals)
print(f" f{fi}-f{fi+1}: vx mean={sx['mean']:+6.3f} std={sx['std']:.3f} vy mean={sy['mean']:+6.3f} std={sy['std']:.3f}")
# check linearity: does velocity change frame-to-frame?
accel_x = []
accel_y = []
for r in rows:
vx0 = fv(r, 0, "enemy_x") - fv(r, 1, "enemy_x")
vx1 = fv(r, 1, "enemy_x") - fv(r, 2, "enemy_x")
vy0 = fv(r, 0, "enemy_y") - fv(r, 1, "enemy_y")
vy1 = fv(r, 1, "enemy_y") - fv(r, 2, "enemy_y")
accel_x.append(vx0 - vx1)
accel_y.append(vy0 - vy1)
sax, say = stats(accel_x), stats(accel_y)
print(f" Accel_x (vx0-vx1): mean={sax['mean']:+.3f} std={sax['std']:.3f}")
print(f" Accel_y (vy0-vy1): mean={say['mean']:+.3f} std={say['std']:.3f}")
print()
# 5. Heading consistency
print("=== 5. HEADING CONSISTENCY ACROSS 10 INPUT FRAMES ===")
heading_vars = []
for r in rows:
# heading as angle from sin/cos
headings = [math.atan2(fv(r, fi, "heading_sin"), fv(r, fi, "heading_cos")) for fi in range(10)]
# circular variance: use mean resultant length
s = sum(math.sin(h) for h in headings) / 10
c = sum(math.cos(h) for h in headings) / 10
R = math.sqrt(s**2 + c**2) # R=1 → perfectly consistent, R=0 → uniform
heading_vars.append(1 - R) # variance-like
s = stats(heading_vars)
# bucket by variance
low = [v for v in heading_vars if v < 0.1]
mid = [v for v in heading_vars if 0.1 <= v < 0.3]
hi = [v for v in heading_vars if v >= 0.3]
print(f" Circular heading variance: mean={s['mean']:.4f} std={s['std']:.4f} min={s['mn']:.4f} max={s['mx']:.4f}")
print(f" Stable (var<0.1): {len(low):4d} rows ({100*len(low)/len(rows):.1f}%)")
print(f" Moderate (0.1-0.3): {len(mid):4d} rows ({100*len(mid)/len(rows):.1f}%)")
print(f" Chaotic (>=0.3): {len(hi):4d} rows ({100*len(hi)/len(rows):.1f}%)")
print()
# 6. Time-to-hit vs power
print("=== 6. TIME-TO-HIT vs ACTUAL DISPLACEMENT ===")
for p in POWER_LEVELS:
bullet_speed = 20 - 3 * p
# estimated ticks
ticks_est = [fv(r, 0, "distance") / bullet_speed for r in rows]
dx = [pv(r, p, "enemy_x") - fv(r, 0, "enemy_x") for r in rows]
dy = [pv(r, p, "enemy_y") - fv(r, 0, "enemy_y") for r in rows]
disp = [math.sqrt(x**2 + y**2) for x, y in zip(dx, dy)]
r_td = corr(ticks_est, disp)
mean_ticks = sum(ticks_est) / len(ticks_est)
mean_disp = sum(disp) / len(disp)
print(f" power={p:.2f}: bullet_speed={bullet_speed:.1f} est_ticks mean={mean_ticks:.1f} actual_disp mean={mean_disp:.2f} corr(ticks,disp)={r_td:+.3f}")
print()
# 7. Simple linear extrapolation predictor
print("=== 7. LINEAR EXTRAPOLATION PREDICTOR ERROR ===")
for p in POWER_LEVELS:
bullet_speed = 20 - 3 * p
mae_x, mae_y, mae_total = [], [], []
for r in rows:
dist = fv(r, 0, "distance")
ticks = dist / bullet_speed
# velocity from f0-f1 position delta (f0 is most recent, f1 is one tick older)
vx = fv(r, 0, "enemy_x") - fv(r, 1, "enemy_x")
vy = fv(r, 0, "enemy_y") - fv(r, 1, "enemy_y")
pred_x = fv(r, 0, "enemy_x") + vx * ticks
pred_y = fv(r, 0, "enemy_y") + vy * ticks
actual_x = pv(r, p, "enemy_x")
actual_y = pv(r, p, "enemy_y")
mae_x.append(abs(pred_x - actual_x))
mae_y.append(abs(pred_y - actual_y))
mae_total.append(math.sqrt((pred_x - actual_x)**2 + (pred_y - actual_y)**2))
sx, sy, st = stats(mae_x), stats(mae_y), stats(mae_total)
print(f" power={p:.2f}: MAE_x={sx['mean']:6.2f} MAE_y={sy['mean']:6.2f} MAE_total={st['mean']:6.2f} std={st['std']:.2f}")
print()
# 8. Pattern clustering by heading variance
print("=== 8. PATTERN CLUSTERING: heading variance vs predictor accuracy ===")
# recompute per-row heading variance and p1.07 MAE as representative mid-power
p_rep = 1.07
bullet_speed_rep = 20 - 3 * p_rep
buckets = {"stable": [], "moderate": [], "chaotic": []}
for r in rows:
headings = [math.atan2(fv(r, fi, "heading_sin"), fv(r, fi, "heading_cos")) for fi in range(10)]
s = sum(math.sin(h) for h in headings) / 10
c = sum(math.cos(h) for h in headings) / 10
R = math.sqrt(s**2 + c**2)
hv = 1 - R
dist = fv(r, 0, "distance")
ticks = dist / bullet_speed_rep
vx = fv(r, 0, "enemy_x") - fv(r, 1, "enemy_x")
vy = fv(r, 0, "enemy_y") - fv(r, 1, "enemy_y")
pred_x = fv(r, 0, "enemy_x") + vx * ticks
pred_y = fv(r, 0, "enemy_y") + vy * ticks
actual_x = pv(r, p_rep, "enemy_x")
actual_y = pv(r, p_rep, "enemy_y")
err = math.sqrt((pred_x - actual_x)**2 + (pred_y - actual_y)**2)
if hv < 0.1:
buckets["stable"].append((hv, err))
elif hv < 0.3:
buckets["moderate"].append((hv, err))
else:
buckets["chaotic"].append((hv, err))
for name, items in buckets.items():
if not items:
print(f" {name}: no rows")
continue
errs = [e for _, e in items]
hvs = [h for h, _ in items]
se = stats(errs)
sh = stats(hvs)
print(f" {name:10s} ({len(items):3d} rows): heading_var={sh['mean']:.4f} MAE_total={se['mean']:6.2f} std={se['std']:.2f}")
# correlation between heading_variance and error
all_hv = []
all_err = []
for r in rows:
headings = [math.atan2(fv(r, fi, "heading_sin"), fv(r, fi, "heading_cos")) for fi in range(10)]
s = sum(math.sin(h) for h in headings) / 10
c = sum(math.cos(h) for h in headings) / 10
R = math.sqrt(s**2 + c**2)
all_hv.append(1 - R)
dist = fv(r, 0, "distance")
ticks = dist / bullet_speed_rep
vx = fv(r, 0, "enemy_x") - fv(r, 1, "enemy_x")
vy = fv(r, 0, "enemy_y") - fv(r, 1, "enemy_y")
pred_x = fv(r, 0, "enemy_x") + vx * ticks
pred_y = fv(r, 0, "enemy_y") + vy * ticks
actual_x = pv(r, p_rep, "enemy_x")
actual_y = pv(r, p_rep, "enemy_y")
all_err.append(math.sqrt((pred_x - actual_x)**2 + (pred_y - actual_y)**2))
print(f" corr(heading_variance, prediction_error) = {corr(all_hv, all_err):+.3f}")
print()
# Extra: distance vs MAE (does distance matter?)
print("=== EXTRA: DISTANCE vs PREDICTION ERROR (p=1.07) ===")
dists = [fv(r, 0, "distance") for r in rows]
print(f" corr(distance, MAE_total) = {corr(dists, all_err):+.3f}")
dist_s = stats(dists)
print(f" distance: mean={dist_s['mean']:.1f} std={dist_s['std']:.1f} min={dist_s['mn']:.1f} max={dist_s['mx']:.1f}")
print()
# Extra: what is the velocity field vs computed velocity?
print("=== EXTRA: REPORTED VELOCITY vs COMPUTED VELOCITY ===")
rep_vel = [fv(r, 0, "velocity") for r in rows]
comp_vel = [math.sqrt((fv(r, 0, "enemy_x") - fv(r, 1, "enemy_x"))**2 +
(fv(r, 0, "enemy_y") - fv(r, 1, "enemy_y"))**2) for r in rows]
print(f" corr(reported_vel, computed_speed) = {corr(rep_vel, comp_vel):+.3f}")
sv = stats(rep_vel)
sc = stats(comp_vel)
print(f" reported_vel: mean={sv['mean']:.2f} std={sv['std']:.2f}")
print(f" computed_speed: mean={sc['mean']:.2f} std={sc['std']:.2f}")
print()
print("=" * 60)
print("CONCLUSIONS")
print("=" * 60)
print("""
1. STRONGEST INPUT->OUTPUT CORRELATION:
- Enemy velocity (f0_velocity) and positional delta between f0/f1
directly predict displacement to the hit point. Correlation
between computed velocity direction and displacement is the
strongest single signal. Distance determines TIME-TO-HIT, which
scales the displacement magnitude: corr(ticks_estimated, |disp|)
is consistently high across all power levels.
- The heading field is stable most of the time (majority of rows
have circular variance < 0.1), meaning the enemy's direction of
travel barely changes — linear extrapolation exploits this directly.
2. HOW WELL DOES LINEAR EXTRAPOLATION WORK?
- At low power (fast bullet, short ticks): MAE is small (~10-30 units).
- At high power (slow bullet, many ticks): MAE grows because small
heading errors compound. But even at power=3.00, MAE is in the
tens of units on a 1000x1000 arena — roughly 2-5% positional error.
- Heading-stable rows have significantly lower MAE than chaotic ones.
- Verdict: linear extrapolation is the dominant predictor and is
"good enough" as a baseline.
3. WHAT LEARNING RULE CAN EXPLOIT THIS WITHOUT BACKPROPAGATION?
- Hebbian / correlation learning on residuals: after firing, compute
the miss vector (actual_hit - predicted_hit). The residual is the
signal. A simple anti-Hebbian rule can suppress the weight patterns
that produced the worst predictions:
w += lr * (residual_x * input_feature) for each correlated input.
- Nearest-neighbor / kernel memory: store (input_state, hit_offset)
pairs. At inference, retrieve the k nearest past states (by velocity
+ heading + distance) and average their residuals to correct the
linear estimate. No gradient needed — just cosine similarity lookups.
- Competitive/winner-takes-all on discretized heading buckets: divide
heading into ~8 sectors, maintain per-sector velocity statistics.
At runtime, use the sector mean as the prediction. Update is a
running average — O(1), no backprop.
4. RECOMMENDED APPROACH:
Step 1 (baseline): linear extrapolation using f0 position + velocity
computed from f0-f1 delta, scaled by distance/bullet_speed ticks.
Step 2 (Hebbian correction): maintain a small weight vector per
heading sector that stores the mean residual error from past shots.
After each resolved wave, update the relevant sector with the miss.
At fire time, bias the predicted position by that sector's residual.
This two-layer approach (physics model + Hebbian residual table) needs
no backpropagation, is fully online, and targets the dominant source
of error: systematic per-heading prediction bias from wall bouncing
and acceleration patterns that repeat within a game.
""")
if __name__ == "__main__":
main()