1ed7797cb6
- 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
311 lines
14 KiB
Python
311 lines
14 KiB
Python
"""
|
|
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()
|