#!/usr/bin/env python3
"""Paired native pressure, blue-line decisions, relay and one-timer execution.
Only nine setup frames; no forced actions, movement resets or score injection.
"""
from pathlib import Path
from fixture_readiness import coherent_fixture_source
from concurrent.futures import ThreadPoolExecutor
import argparse,json,os,subprocess,statistics
ap=argparse.ArgumentParser(description=__doc__);ap.add_argument('rom',type=Path);ap.add_argument('output',type=Path);ap.add_argument('--baseline',type=Path,required=True);ap.add_argument('--state',type=Path,required=True);ap.add_argument('--legacy-point-fixture',action='store_true',help='reproduce original point case with unrelated receivers and pressure');a=ap.parse_args();h=Path(__file__).resolve().parent;o=a.output.resolve();o.mkdir(parents=True,exist_ok=True)
specs=[dict(kind=k,team=t,direction=d,difficulty=1) for k in ['pressure','point','relay','finish'] for t in [0,1] for d in [-1,1]]
specs += [dict(kind=k,team=0,direction=d,difficulty=level) for k in ['pressure','point','relay'] for d in [-1,1] for level in [0,2]]
specs += [dict(kind='point',team=t,direction=d,difficulty=1,blocked=True) for t in [0,1] for d in [-1,1]]
# Exercise two additional relay lanes outside the initial marker's reach.
# Keep all original32 scenarios and every existing strict success criterion.
specs += [dict(kind='relay',team=t,direction=d,difficulty=1,relay_x=x) for t in [0,1] for d in [-1,1] for x in [100,110]]
# Isolate the clear point-shot decision during setup. Relay/finish/pressure
# cases and every numerical success criterion remain unchanged.
for spec in specs:
 if spec['kind']=='point' and not a.legacy_point_fixture:spec['isolate_point']=True
fixture_hook=o/'coherent-fixture-hook.py';fixture_hook.write_text(coherent_fixture_source((h/'ai_playbook_gameplay_hook.py').read_text()))
def run(profile,rom,index,s):
 p=o/(profile+'-'+str(index));p.mkdir(exist_ok=True);(p/'spec.json').write_text(json.dumps(s));(p/'commands.txt').write_text(f'100 load {a.state.resolve()}\n148 snap\n188 snap\n248 snap\n708 snap\n709 exit\n');env={k:v for k,v in os.environ.items() if not k.startswith('GPGX_')};env.update(GPGX_DEV='513',GPGX_HOOK=str(fixture_hook))
 with (p/'log.txt').open('w') as log:subprocess.run(['nice','-n','19','python3',str(h/'test_core.py'),str(rom.resolve()),str(p/'commands.txt'),str(p)],env=env,stdout=log,stderr=log,check=True,timeout=30)
 r=json.loads((p/'trajectory.json').read_text());assert len(r)==600 and 0<r[-1]['clock']<r[0]['clock'] and all(x['sprite_bytes']<=0x280 for x in r),('stalled play/sprite overflow',s)
 assert all(x['controller_teams']==r[0]['controller_teams'] for x in r[:120]),('controller team stolen',s)
 c=r[0]['carrier'];own=s['team']*6;passes=[];shots=[]
 previous=None
 for row in r:
  if row['owner']>11 and own<=row['passer']<own+5 and own<=row['target']<own+5 and (not passes or passes[-1]['passer']!=row['passer'] or passes[-1]['target']!=row['target']):
   passes.append(dict(frame=row['frame'],passer=row['passer'],target=row['target']))
  if own<=row['shooter']<own+5 and row['owner']>11 and (not shots or shots[-1]['shooter']!=row['shooter']):
   i=row['shooter'];buffered=previous is not None and previous['cpu_pending']==i+1 and row['cpu_pending']==0 and row['animations'][i]==0x7fc
   shots.append(dict(frame=row['frame'],shooter=i,one_timer=row['one_timer'][i] or buffered,buffered=buffered,power=row['shot_power']))
  previous=row
 windup=next((x for x in r if x['jobs'][c]==18),None)
 # A real relay catch must precede its second pass and native one-timer shot.
 chain=None
 for first,second in zip(passes,passes[1:]):
  caught=any(x['owner']==first['target'] for x in r if first['frame']<x['frame']<second['frame'])
  final=next((x for x in shots if x['frame']>second['frame'] and x['shooter']==second['target'] and x['one_timer']),None)
  if caught and second['passer']==first['target'] and final:chain=dict(first=first,second=second,shot=final);break
 return dict(gap60=r[59]['nearest'],gap_mean=statistics.mean(x['nearest'] for x in r[29:60]),windup_depth=windup['positions'][c][1] if windup else None,passes=passes,shots=shots,chain=chain,positions60=r[59]['positions'],first_catch=min([x['frame'] for x in r if x['owner']==own]+[x['frame'] for x in shots if x['shooter']==own and x['buffered']],default=None))
def pair(item):
 i,s=item;old=run('old',a.baseline,i,s);new=run('new',a.rom,i,s)
 if s['kind']=='pressure':
  gain=old['gap60']-new['gap60']
  if s['difficulty']==1:assert gain>15,('stationary pressure did not improve',s,old,new)
  # Native hardware RNG is sampled at CPU-cycle times; guard overhead can
  # change a native burst roll even on Normal/Easy. Exact branch/register/RNG
  # guards are checked by verify_ai_playbook; live controls remain recorded.
 return dict(spec=s,baseline=old,fixed=new)
with ThreadPoolExecutor(max_workers=3) as pool:results=list(pool.map(pair,enumerate(specs)))
pressure=[r for r in results if r['spec']['kind']=='pressure' and r['spec']['difficulty']==1]
points=[r for r in results if r['spec']['kind']=='point' and r['spec']['difficulty']==1 and not r['spec'].get('blocked')]
relays=[r for r in results if r['spec']['kind']=='relay' and r['spec']['difficulty']==1]
finishes=[r for r in results if r['spec']['kind']=='finish']
early=lambda profile:sum(r[profile]['windup_depth'] is not None and 88<=r[profile]['windup_depth']<=144 for r in points)
summary=dict(scenarios=len(results),mean_pressure_gain=statistics.mean(r['baseline']['gap60']-r['fixed']['gap60'] for r in pressure),old_blue_windups=early('baseline'),new_blue_windups=early('fixed'),old_relay_catches=sum(r['baseline']['first_catch'] is not None and r['baseline']['first_catch']<100 for r in relays),new_relay_catches=sum(r['fixed']['first_catch'] is not None and r['fixed']['first_catch']<100 for r in relays),old_relay_chains=sum(r['baseline']['chain'] is not None for r in relays),new_relay_chains=sum(r['fixed']['chain'] is not None for r in relays),baseline_finish_releases=sum(any(x['one_timer'] for x in r['baseline']['shots']) for r in finishes),native_finish_releases=sum(any(x['one_timer'] for x in r['fixed']['shots']) for r in finishes))
(o/'results.json').write_text(json.dumps(results,indent=2)+'\n');(o/'summary.json').write_text(json.dumps(summary,indent=2)+'\n')
assert summary['mean_pressure_gain']>25 and summary['new_blue_windups']>=3 and summary['new_blue_windups']>summary['old_blue_windups'],summary
assert summary['new_relay_catches']>=2 and summary['new_relay_catches']>=summary['old_relay_catches'] and summary['new_relay_chains']>=1,summary
assert summary['native_finish_releases']>=2 and summary['native_finish_releases']>summary['baseline_finish_releases'],summary
print('PASS paired native playbook gameplay;',json.dumps(summary))
