from pathlib import Path import re,json,math,shutil,sys,hashlib root=Path('/home/jakub/devel/escapistsrereintroduced');out=root/'docs/evidence/uaz-coordination-2026-10-04';sys.path.insert(0,str(root/'tools')) from source_manifest import addon_manifest runs={'baseline':Path('/tmp/erri-uaz-scene-native/baseline'),'manoeuvre':Path('/tmp/erri-uaz-scene-native/final'),'final':Path('/tmp/erri-uaz-scene-native/qualified')} allmetrics={};traces={} for name,run in runs.items(): session=next((run/'profile/logs').glob('logs_*'));s=(session/'console.log').read_text(errors='replace');j=(session/'rereintroduced-hq.log').read_text(errors='replace') traces[name]={} for match in re.finditer(r'ERR SCENE MAN: t=([\d.]+) id=(\S+) man=(\d+) pos=<([^>]+)> mounted=(\d) alive=(\d)',s): t,unit,member,xyz,mounted,alive=match.groups();x,y,z=map(float,xyz.split(','));traces[name].setdefault((unit,int(member)),[]).append((float(t),x,z,int(mounted))) metrics={} for unit in sorted({u for u,m in traces[name]}): chars={m:v for (u,m),v in traces[name].items() if u==unit};leader=chars.get(0,[]);first_mount=min((v[0] for v in leader if v[3]),default=float('inf'));foot=[v for v in leader if not v[3] and v[0]>first_mount] ammo=[int(m[1]) for m in re.findall(r'ERR SCENE AMMO: t=([\d.]+) id='+re.escape(unit)+r' rounds=(\d+)',s)] metrics[unit]={'members_observed':len(chars),'members_seen_on_foot':sum(any(not p[3] and p[0]>min((m[0] for m in v if m[3]),default=float('inf')) for p in v) for v in chars.values()),'leader_max_displacement_on_foot_m':round(max((math.hypot(p[1]-foot[0][1],p[2]-foot[0][2]) for p in foot),default=0),1),'mounted_rounds_start':ammo[0] if ammo else None,'mounted_rounds_min':min(ammo) if ammo else None} allmetrics[name]={'complete':'ERR SCENE COMPLETE' in s,'units':metrics,'script_errors':[l for l in s.splitlines() if 'SCRIPT (E)' in l],'tactical_events':[l for l in j.splitlines() if 'HUNTER TACTICS' in l or 'HUNTER COORDINATION' in l]} for src,dst in [(session/'console.log',out/(name+'-console.log')),(run/'result.json',out/(name+'-result.json'))]: if src.exists():shutil.copy2(src,dst) (out/(name+'-events.log')).write_text('\n'.join(l for l in j.splitlines() if any(w in l for w in ['HUNTER','CONTACT REPORT','CONTACT LOCAL']))+'\n') manifest={str(p.relative_to(run/'source')):hashlib.sha256(p.read_bytes()).hexdigest() for p in (run/'source').rglob('*.c')} (out/(name+'-script-sha256.json')).write_text(json.dumps(manifest,indent=2)+'\n') (out/'metrics.json').write_text(json.dumps(allmetrics,indent=2)+'\n') # A spatial reconstruction of logged positions, not a game screenshot. svg=['','','','Native UAZ encounter — recorded movement on Nogova','Protected US AI stand-ins; real production hunter crews, perception, radio and navigation. No terrain background.'] colors=['#d97706','#2563eb','#9d28b0'];origin=(9606.07,4826.07) for panel,name in enumerate(['baseline','final']): left=40+panel*615;top=110;size=540;scale=.61 def xy(x,z):return left+size/2+(x-origin[0])*scale,top+size/2-(z-origin[1])*scale svg.append(f''+('Original orders (2 UAZs)' if name=='baseline' else 'Coordinated orders (3 UAZs)')+'') svg.append(f'') for dist in range(-400,401,100): x,y=xy(origin[0]+dist,origin[1]+dist);svg.extend([f'']) svg.append(f'') cx,cy=xy(*origin);svg.append(f'Initial escapee area') units=sorted({u for u,m in traces[name]}) for i,unit in enumerate(units): color=colors[i] for (u,member),points in traces[name].items(): if u!=unit:continue # Limit comparison to the first 210 seconds shared by both recordings. points=[p for p in points if p[0]<=210] for mounted in [True,False]: segments=[];current=[] for p in points: if bool(p[3])==mounted:current.append(p) elif current:segments.append(current);current=[] if current:segments.append(current) for segment in segments: coords=' '.join(f'{x:.1f},{y:.1f}' for x,y in [xy(p[1],p[2]) for p in segment]);dash=' stroke-dasharray="4 3"' if mounted else '' svg.append(f'') if points: x,y=xy(points[-1][1],points[-1][2]);svg.append(f'') svg.append(f'UAZ {i+1} / original crew') svg.append('') svg.append(f'First 210 seconds · 100 m grid · dashed: mounted · solid: on foot') svg.append('');(out/'movement.svg').write_text('\n'.join(svg)) (out/'source-sha256.json').write_text(json.dumps(addon_manifest(root),indent=2)+'\n') print(json.dumps({n: {'complete':v['complete'],'units':v['units']} for n,v in allmetrics.items()},indent=2))