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:
@@ -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()
|
||||
Reference in New Issue
Block a user