#!/usr/bin/env python3
"""Optional native Hard shot planning, exact native decisions/targets and storage.
Declared geometry/input fixtures never count as win-rate or human playtests.
"""
from pathlib import Path
from menu_protocol import native_down_count,row_word,down_count,render_probe,requested_menu_expectations
from concurrent.futures import ThreadPoolExecutor
import argparse,hashlib,json,os,struct,subprocess
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('--seed',type=Path,required=True);ap.add_argument('--state',type=Path,required=True);ap.add_argument('--section',choices=['all','geometry','decision','target','commit','flow','input','storage','menu','lifetime'],default='all');ap.add_argument('--flow-reference',type=Path,help='exact R7 before menu/replay graphics, for a genuine matched native pre-outcome start');a=ap.parse_args();h=Path(__file__).resolve().parent;o=a.output.resolve();o.mkdir(exist_ok=True,parents=True);env={k:v for k,v in os.environ.items() if not k.startswith('GPGX_')};sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest();assert sha(a.baseline)=='984854649bb9528f9dfc2200d43237cb2a3ab2cbe5cc847fde1ef10d375e660f'
_native=None
def asm(p,body,origin=0x2d5000):
 (p/'driver.s').write_text(('' if '.globl _start' in body else ' .text\n .globl _start\n_start:\n')+body+'\n');subprocess.run([str(h.parent/'m68kasm.sh'),str(p/'driver.s'),f'{origin:X}',str(p/'code.bin')],stdout=subprocess.DEVNULL,check=True);return(p/'code.bin').read_bytes()

def run(p,body,size,patches=(),profile='current'):
 p.mkdir(exist_ok=True,parents=True);b=bytearray((a.baseline if profile=='parent' else a.rom).read_bytes());code=asm(p,' move.w #0x2700,%sr\n movea.l #0xfffff900,%sp\n'+body+'\n move.l #0x50444c59,0xffff3ff0\nend: bra.s end');assert len(code)<0x4000 and b[0x2d5000:0x2d5000+len(code)]==b'\xff'*len(code);b[0x2d5000:0x2d5000+len(code)]=code;struct.pack_into('>I',b,4,0x2d5000)
 for at,v in patches:b[at:at+len(v)]=v
 (p/'rom.bin').write_bytes(b);(p/'commands.txt').write_text(f'1500 dump ff3ff0 4 done\n1500 dump ff4000 {size} result\n1501 exit\n')
 with(p/'log').open('w') as f:subprocess.run(['python3',str(h/'test_core.py'),str(p/'rom.bin'),str(p/'commands.txt'),str(p)],env=env|({'GPGX_SRM':str(p/'battery.srm')} if (p/'battery.srm').exists() else {}),stdout=f,stderr=f,check=True,timeout=90)
 rows={t[1]:bytes.fromhex(' '.join(t[2:])) for l in(p/'commands.out').read_text().splitlines() if(t:=l.split())};assert rows['done']==b'PDLY',('native return',p);return rows['result']

def pack(b):
 for off in range(0,len(b),8192):b[off+0x1fff]=sum(b[off:off+0x1ffe])&255;b[off+0x1ffe]=b[off+0x1fff]^255
 raw=bytearray(2*len(b));raw[1::2]=b;return raw

def game_core(p,rom,commands,settings):
 (p/'commands.txt').write_text('\n'.join(commands)+'\n')
 with(p/'log').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|settings,stdout=log,stderr=log,check=True,timeout=100)

def snapshot(at):
 return [f' movem.l %d0-%d7/%a0-%a6,0x{at:x}',f' move.w %sr,0x{at+60:x}',f' move.l %sp,0x{at+62:x}']

def abi(raw):return dict(regs=list(struct.unpack_from('>15I',raw)),sr=int.from_bytes(raw[60:62],'big'),sp=int.from_bytes(raw[62:66],'big'))

def registers(a2=0xffffb2ca,a3=0xffffb74a,d0=0x12340003,d1=0x432100fc):
 regs=[0x11223300+i for i in range(8)]+[0xffff1000+256*i for i in range(7)];regs[0]=d0;regs[1]=d1;regs[10]=a2;regs[11]=a3
 lines=[f' move.l #0x{x:x},%d{i}' for i,x in enumerate(regs[:8])]+[f' movea.l #0x{x:x},%a{i}' for i,x in enumerate(regs[8:])];return lines,regs

def memory_fixture():
 assert a.state and a.state.is_file(),'--state is required for initialized native save/physics fixtures'
 p=o/'native-memory';p.mkdir(exist_ok=True);(p/'commands.txt').write_text(f'100 load {a.state.resolve()}\n101 dump ff0000 65536 ram\n102 exit\n')
 with (p/'log').open('w') as log:subprocess.run(['python3',str(h/'test_core.py'),str(a.baseline.resolve()),str(p/'commands.txt'),str(p)],env=env|dict(GPGX_DEV='1281'),stdout=log,stderr=log,check=True,timeout=30)
 values={t[1]:bytes.fromhex(' '.join(t[2:])) for line in (p/'commands.out').read_text().splitlines() if (t:=line.split())};assert len(values['ram'])==65536
 raw=values['ram'];(p/'native-ram.bin').write_bytes(raw);return raw

def native_payload():
 global _native
 if _native is None:_native=memory_fixture()[0xb000:0xf850]
 return _native

def copy_native():
 return [' movea.l #0xfffffe00,%sp',' lea 0x230000,%a0',' lea 0xffffb000,%a1',' move.w #0x4850/4-1,%d0','copy_native: move.l (%a0)+,(%a1)+',' dbra %d0,copy_native']

def fixture(z):
 team=z.get('team',0);sign=z.get('sign',1);actor=2+6*team;obj=0xffffb04a+128*actor;gk=11 if team==0 else 5;goal=0xffffb04a+128*gk;flags=64*team+(128 if sign>0 else 0);x,y=z.get('puck',[0,160]);gx,gy=z.get('keeper',[0,246]);body=copy_native()
 for i in range(12):body += [f' move.w #-1,0x{0xffffb04a+128*i+0x34:x}']
 body += [f' lea 0x{obj:x},%a3',f' move.w #{actor},0x52(%a3)',f' move.w #4,0x34(%a3)',f' move.b #{flags},0x62(%a3)',' clr.b 0x63(%a3)',' clr.b 0x64(%a3)',' move.b #15,0x66(%a3)',f' move.w #{x},(%a3)',f' move.w #{y*sign},0x14(%a3)',f' move.w #0,0x{goal+0x34:x}',f' move.w #{gx},0x{goal:x}',f' move.w #{gy*sign},0x{goal+0x14:x}',f' clr.l 0x{goal+0x28:x}',f' move.b #{64*(1-team)},0x{goal+0x62:x}',f' clr.b 0x{goal+0x63:x}',f' move.w #{x},0xffffb74a',f' move.w #{y*sign},0xffffb75e',f' move.w #{actor},0xffffb7aa',' move.w #1,0xfffff810',' move.w #1,0xfffff07a',' clr.w 0xffffbedc',' clr.b 0xffffc2ea',' clr.b 0xffffc2ec',' clr.b 0xffffc2ee',' clr.b 0xffffc2f2',' clr.b 0xffffc2fa',' clr.w 0xffffd412',' clr.w 0xfffff738',' move.l #0xffffffff,0xffffc320',' move.l #0xffffffff,0xffffc324',' clr.l 0xffffc328',' clr.l 0xffffc32c',' move.l #0xa55a5aa5,0xfffff840',' move.l #0x5aa5a55a,0xfffff844']
 edits={'Off':[' clr.w 0xfffff810'],'invalid-option':[' move.w #99,0xfffff810'],'Normal':[' clr.w 0xfffff07a'],'Easy':[' move.w #2,0xfffff07a'],'human':[' bset #3,0x62(%a3)'],'busy':[' bset #5,0x62(%a3)'],'keeper':[' clr.w 0x34(%a3)'],'unavailable':[' move.w #-1,0x34(%a3)'],'bad-role':[' move.w #6,0x34(%a3)'],'boxed':[' bset #2,0x63(%a3)'],'fighting':[' bset #0,0x63(%a3)'],'whistle':[' move.b #1,0xffffc2ea'],'replay':[' move.b #16,0xffffc2ea'],'gameover':[' move.b #64,0xffffc2ea'],'paused':[' move.b #128,0xffffc2ec'],'foreign-view':[' move.b #1,0xffffc2ec'],'offside':[' move.b #128,0xffffc2ee'],'attempt':[' move.b #4,0xffffc2f2'],'shootout':[' move.b #1,0xffffc2fa'],'shootout-flight':[' move.b #4,0xffffc2fa'],'penalty':[' move.w #1,0xffffd412'],'loose':[' move.w #-1,0xffffb7aa'],'other-owner':[f' move.w #{(actor+6)%12},0xffffb7aa'],'bad-index':[' move.w #12,0x52(%a3)'],'negative-index':[' move.w #-1,0x52(%a3)'],'buffered':[' bset #3,0x64(%a3)'],'pass-state':[' bset #1,0x64(%a3)'],'no-keeper':[f' move.w #-1,0x{goal+0x34:x}']}
 kind=z.get('guard','live');body+=edits.get(kind,[])
 if kind.startswith('pad-'):
  pad=int(kind.split('-')[1]);body += [f' move.w #{team+1},0x{0xffffc328+2*pad:x}',f' move.w #{actor},0x{0xffffc320+2*pad:x}']
 if kind.startswith('replay-tag-'):body += [f' move.w #{0xa502+int(kind.split("-")[-1])},0xfffff738']
 return body,obj

def geometry_suite():
 examples=[dict(name='long-point',puck=[0,88],want=0),dict(name='long-boundary',puck=[0,104],want=0),dict(name='inside-boundary',puck=[0,105],want=1),dict(name='covered-slot',puck=[0,160],want=1),dict(name='covered-close-approach',puck=[0,200],want=1),dict(name='close-finish',puck=[0,252],want=0),dict(name='real-keeper-opening',puck=[0,160],keeper=[80,246],want=0),dict(name='empty-net',puck=[0,160],guard='no-keeper',want=0),dict(name='behind-goal',puck=[0,270],want=0),dict(name='native-neutral-clear',puck=[0,-80],want=0),dict(name='wide-long-native',puck=[-160,180],want=0),dict(name='invalid-x',puck=[161,180],want=0)]
 cases=[dict(z,team=team,sign=sign) for z in examples for team in [0,1] for sign in [-1,1]]
 guards=['Off','invalid-option','Normal','Easy','human','busy','keeper','unavailable','bad-role','boxed','fighting','whistle','replay','gameover','paused','foreign-view','offside','attempt','shootout','shootout-flight','penalty','loose','other-owner','bad-index','negative-index','buffered','pass-state']+['pad-'+str(n) for n in range(4)]+['replay-tag-'+str(n) for n in range(5)]
 cases += [dict(name='guard-'+g,guard=g,want=0,team=team,sign=1) for g in guards for team in [0,1]]
 payload=native_payload()
 def trial(item,profile):
  i,z=item;body,obj=fixture(z);init,regs=registers(a3=obj);body+=init+[' move.w #0x271f,%sr',f' jsr 0x{0x22300c if profile=="current" else 0x15464:x}']+snapshot(0xffff4000)+[' lea 0xffffb000,%a0',' lea 0xffff4042,%a1',' move.w #0x4850/4-1,%d0','snap: move.l (%a0)+,(%a1)+',' dbra %d0,snap'];return run(o/f'geometry-{profile}-{i}','\n'.join(body),66+len(payload),[(0x230000,payload)],profile),regs
 def pair(item):
  i,z=item;(old,_),(new,regs)=trial(item,'parent'),trial(item,'current');ctx=abi(new);regs[0]=z['want'];assert ctx['regs']==regs and ctx['sp']==0xfffffe00 and ctx['sr']==(0x2710 if z['want'] else 0x2714),('actual native geometry/guard result or full ABI',z,ctx,regs);assert old[66:]==new[66:],('geometry wrote actual gameplay/BEDC/own metadata/RNG',z,[hex(0xb000+n) for n,(x,y) in enumerate(zip(old[66:],new[66:])) if x!=y][:24]);return z|dict(full_native_RAM_RNG_BEDC_and_other_registers_exact=True)
 with ThreadPoolExecutor(max_workers=3) as pool:rows=list(pool.map(pair,enumerate(cases)))
 print('PASS',len(rows),'actual native aim/keeper/distance boundary and both-team/end full-ABI guarded geometry contexts',flush=True);return dict(cases=rows)


def state_lines(z,actor):
 if z.get('state')=='valid':
  return [' move.w #0x4150,0xfffff840',f' move.w #{z.get("record_actor",actor)},0xfffff842',f' move.w #{z.get("age",0)},0xfffff844',f' move.w #{z.get("side",1)},0xfffff846']
 return []

def decision_suite():
 cases=[dict(team=team,age=age,state='valid',want=int(age!=96)) for team in [0,1] for age in [0,1,94,95,96,97,65535]]
 cases += [dict(team=0,state='valid',age=96,record_actor=8,want=1),dict(team=0,state='valid',age=96,side=0,want=1),dict(team=0,state='invalid',age=96,want=1),dict(team=0,state='valid',age=96,guard='Off',want=0)]
 payload=native_payload()
 def trial(item,profile):
  i,z=item;body,obj=fixture(z);body+=state_lines(z,2+6*z['team']);init,regs=registers(a3=obj);body+=init+[' move.w #0x271f,%sr',f' jsr 0x{0x223008 if profile=="current" else 0x15464:x}']+snapshot(0xffff4000)+[' lea 0xffffb000,%a0',' lea 0xffff4042,%a1',' move.w #0x4850/4-1,%d0','snap: move.l (%a0)+,(%a1)+',' dbra %d0,snap'];return run(o/f'decision-{profile}-{i}','\n'.join(body),66+len(payload),[(0x230000,payload)],profile),regs
 def pair(item):
  i,z=item;(old,_),(new,regs)=trial(item,'parent'),trial(item,'current');ctx=abi(new);regs[0]=z['want'];assert ctx['regs']==regs and ctx['sp']==0xfffffe00 and ctx['sr']==(0x2710 if z['want'] else 0x2714),('bounded pure decision/native ABI',z,ctx);assert old[66:]==new[66:],('decision changed actual game/own record/RNG',z);return z
 with ThreadPoolExecutor(max_workers=3) as pool:rows=list(pool.map(pair,enumerate(cases)))
 print('PASS',len(rows),'bounded age/ownership/corruption native pure-decision and full ABI/RAM contexts',flush=True);return dict(cases=rows)


def target_suite():
 cases=[dict(team=team,sign=sign,actor_xy=[x,y],wanted='route',stage=1 if y<216 else 2) for team in [0,1] for sign in [-1,1] for x in [-20,0,20] for y in [160,216]]
 cases += [dict(team=0,actor_xy=[0,160],wanted='native',pressure=[0,24]),dict(team=0,actor_xy=[0,160],wanted='route',stage=1,pressure=[0,25]),dict(team=0,actor_xy=[0,160],wanted='native',pressure=[16,17]),dict(team=0,actor_xy=[0,160],wanted='route',stage=1,pressure=[17,17]),dict(team=0,actor_xy=[0,160],wanted='route',stage=1,pressure=[0,0],pressure_role=0),dict(team=0,actor_xy=[0,160],wanted='route',stage=1,pressure=[0,0],pressure_boxed=True)]
 cases += [dict(team=team,sign=sign,actor_xy=[0,160],keeper=[0,184],wanted='route',stage=1) for team in [0,1] for sign in [-1,1]]
 cases += [dict(team=team,age=age,state='valid',wanted='native' if age in [95,96] else 'route',stage=1) for team in [0,1] for age in [0,94,95,96,97,65535]]
 cases += [dict(team=0,guard=g,wanted='unchanged') for g in ['Off','Normal','Easy','human','paused','foreign-view','whistle','gameover','shootout','attempt','pad-0','pad-3']]
 cases += [dict(team=0,puck=[0,88],wanted='clear'),dict(team=0,puck=[0,252],wanted='clear'),dict(team=0,state='valid',age=12,side=-1,wanted='route',stage=1)]
 cases += [dict(team=0,wanted='route' if guard=='live' else 'unchanged',stage=1,guard=guard,sr=0x2000+(irq<<8)+ccr) for irq in range(8) for ccr in range(32) for guard in ['live','Off']]
 payload=native_payload();stub=bytes.fromhex('48f97fffffff4e0040f9ffff4e3c23cfffff4e3e4e75')
 def trial(item,profile):
  i,z=item;body,obj=fixture(z);actor=2+6*z['team'];xy=z.get('actor_xy',[0,z.get('puck',[0,160])[1]]);body += [f' move.w #{xy[0]},0x{obj:x}',f' move.w #{xy[1]*z.get("sign",1)},0x{obj+0x14:x}'];body+=state_lines(z,actor)
  if 'pressure' in z:
   opp=6*(1-z['team']);other=0xffffb04a+128*opp;dx,dy=z['pressure'];body += [f' move.w #{z.get("pressure_role",4)},0x{other+0x34:x}',f' move.w #{xy[0]+dx},0x{other:x}',f' move.w #{xy[1]*z.get("sign",1)+dy},0x{other+0x14:x}',f' move.b #{4 if z.get("pressure_boxed") else 0},0x{other+0x63:x}']
  init,regs=registers(a3=obj);body+=init+[f' move.w #{z.get("sr",0x271f)},%sr',' jsr 0xe10c',' lea 0xffff4e00,%a0',' lea 0xffff4000,%a1',' moveq #32,%d0','snap_abi: move.w (%a0)+,(%a1)+',' dbra %d0,snap_abi',' lea 0xffffb000,%a0',' lea 0xffff4042,%a1',' move.w #0x4850/4-1,%d0','snap_ram: move.l (%a0)+,(%a1)+',' dbra %d0,snap_ram'];return run(o/f'target-{profile}-{i}','\n'.join(body),66+len(payload),[(0x230000,payload),(0xe116,stub)],profile),regs
 def pair(item):
  i,z=item;(old,_),(new,regs)=trial(item,'parent'),trial(item,'current');ctx=abi(new);regs[8]=0xe12c
  if z['wanted']=='route':
   side=z.get('side',-1 if z.get('actor_xy',[0])[0]<0 else 1) if z.get('state')=='valid' and z.get('age',0)<=96 else -1 if z.get('actor_xy',[0])[0]<0 else 1;x=side*(56 if z['stage']==1 else 28);y=(220 if z['stage']==1 else 248)*z.get('sign',1);regs[0]=(regs[0]&0xffff0000)|(x&65535);regs[1]=(regs[1]&0xffff0000)|(y&65535)
  assert ctx['regs']==regs and ctx['sp']==0xfffffdfc and ctx['sr']==((z.get('sr',0x271f)&~4)|(0 if z.get('guard')=='attempt' else 4)),('actual native target/bounded pressure route/full GPR/SR/stack',z,ctx,regs)
  before,after=bytearray(old[66:]),bytearray(new[66:]);block=bytes(after[0xf840-0xb000:0xf848-0xb000]);before[0xf840-0xb000:0xf848-0xb000]=block;assert before==after,('target changed actual player positions/velocity/ownership/attributes/clock/score/RNG/BEDC',z,[hex(0xb000+n) for n,(x,y) in enumerate(zip(before,after)) if x!=y][:24])
  if z['wanted']=='unchanged':assert old==new,('guard changed original target or private metadata',z)
  elif z['wanted']=='clear':assert block==bytes(8)
  else:
   magic,actor,age,side=struct.unpack('>4H',block);wanted_age=min(z.get('age',0)+1,96) if z.get('state')=='valid' and z.get('age',0)<=96 else 1;assert magic==0x4150 and actor==2+6*z['team'] and age==wanted_age and side in [1,65535],('bounded own record only',z,block.hex())
  return z|dict(record=block.hex(),full_native_other_RAM_RNG_and_boundary_ABI_exact=True)
 with ThreadPoolExecutor(max_workers=3) as pool:rows=list(pool.map(pair,enumerate(cases)))
 print('PASS',len(rows),'actual native target boundary both teams/ends/legs/stable-side/pressure/age/guard cases',flush=True);return dict(cases=rows)


def commit_suite():
 cases=[dict(team=team,sign=sign,guard=g,decline=g=='live') for team in [0,1] for sign in [-1,1] for g in ['live','Off','Normal','Easy','human','paused','whistle','shootout','buffered','pad-0','pad-3']]
 cases += [dict(team=0,state='valid',age=age,decline=age!=96) for age in [0,95,96,97]]
 cases += [dict(team=0,guard=guard,decline=guard=='live',sr=0x2000+(irq<<8)+ccr) for irq in range(8) for ccr in range(32) for guard in ['live','Off']]
 payload=native_payload();stub=bytes.fromhex('48f97fffffff4e0040f9ffff4e3c23cfffff4e3e4e75')
 def trial(item,profile):
  i,z=item;body,obj=fixture(z);body+=state_lines(z,2+6*z['team']);init,regs=registers(a3=obj);body+=init+[f' move.w #{z.get("sr",0x271f)},%sr',' pea accepted',' jsr 0xe412']+snapshot(0xffff4e00)+[' addq.w #4,%sp',' move.w #0,0xffff3ef0',' bra.s joined','accepted: move.w #1,0xffff3ef0','joined: lea 0xffff4e00,%a0',' lea 0xffff4000,%a1',' moveq #32,%d0','snap_abi: move.w (%a0)+,(%a1)+',' dbra %d0,snap_abi',' move.w 0xffff3ef0,(%a1)+',' lea 0xffffb000,%a0',' lea 0xffff4044,%a1',' move.w #0x4850/4-1,%d0','snap_ram: move.l (%a0)+,(%a1)+',' dbra %d0,snap_ram'];return run(o/f'commit-{profile}-{i}','\n'.join(body),68+len(payload),[(0x230000,payload),(0xe418,bytes.fromhex('4ef9002d4c00')),(0x2d4c00,stub)],profile),regs
 def pair(item):
  i,z=item;(old,_),(new,regs)=trial(item,'parent'),trial(item,'current');ctx=abi(new);decline=z['decline'];accepted=int.from_bytes(new[66:68],'big');assert accepted==int(not decline),('actual native commit/decline continuation',z,accepted)
  if decline:assert ctx['regs']==regs and ctx['sr']==z.get('sr',0x271f) and ctx['sp']==0xfffffdfc,('decline lost caller register/CCR/IRQ/stack',z,ctx)
  else:assert new[:68]==old[:68],('accepted/guard Native E412 discard/MOVE boundary differs',z,abi(old),ctx)
  assert old[68:]==new[68:],('commit changed actual game/own record/BEDC/RNG',z,[hex(0xb000+n) for n,(x,y) in enumerate(zip(old[68:],new[68:])) if x!=y][:24]);return z|dict(native_accepted=bool(accepted),full_native_RAM_and_continuation_ABI=True)
 with ThreadPoolExecutor(max_workers=3) as pool:rows=list(pool.map(pair,enumerate(cases)))
 print('PASS',len(rows),'actual committed-shot native return discard/decline/guard/full-ABI contexts',flush=True);return dict(cases=rows)



def encode(height,speed,mode,cone):
 value=(height+2*speed+4*mode+8*cone)|(0x80 if mode or cone else 0x50);return bytes([value,value^255])

def battery(gp=0,choices=((1,0,1,1),(0,1,0,2),(1,1,1,3)),native=(0,1,1,2),bad=None):
 base=bytearray(a.seed.read_bytes()[1:16384:2]);base[0xe20:0x1ffe]=bytes(0x11de);banks=[]
 for slot,length in [(1,84),(2,31),(3,7)]:
  b=bytearray(base);struct.pack_into('>4s6H',b,0x1000,b'SESN',gp,11 if gp else 0,gp,length,0 if gp else slot,3);struct.pack_into('>4s4H',b,0x1ea0,b'S3SV',1,slot,7,1);b[0x1ef0:0x1ef6]=b'PC\x01\xfe\x01\xfe';b[0x1ff6:0x1ffe]=bytes([0x55,0,0,2,2,0,0x5a,0]);b[0x1eee:0x1ef0]=encode(*choices[slot-1]);banks.append(b)
 b=bytearray(banks[0])+b''.join(banks);b[0x2e18:0x2e20]=b'RP\x01\xfeCT\x01\xfe';b[0x4d20:0x4d22]=encode(*native)
 if bad is not None:
  for bank in [0,0x2000]:b[bank+0x1eee:bank+0x1ef0]=bytes.fromhex(bad)
 return pack(b)

def storage_suite():
 specs=[dict(slot=slot,tag=t,inv=t^255) for slot in range(4) for t in range(256)]+[dict(slot=slot,tag=t,inv=t) for slot in range(4) for t in [0,0x76,0x77,255]]
 def trial(batch):
  ss=specs[batch*16:batch*16+16];body=[' move.b #1,0xa130f1']
  for j,z in enumerate(ss):
   rec=0x209a85+4*z['slot'];dest=0xffff4000+j*8
   body += [f' move.w #{int(z["slot"]>0)},0xfffff06c',f' move.w #{z["slot"]},0xfffff06a',f' move.b #{z["tag"]},0x{rec:x}',f' move.b #{z["inv"]},0x{rec+2:x}',' move.w #99,0xfffff810',' move.w #0x271f,%sr',' jsr 0x223010',f' move.w %sr,0x{dest:x}',f' move.w 0xfffff810,0x{dest+2:x}',f' move.l %sp,0x{dest+4:x}']
  raw=run(o/f'storage-{batch}','\n'.join(body),8*len(ss));out=[]
  for j,z in enumerate(ss):
   sr,value,sp=struct.unpack_from('>HHI',raw,8*j);want=z['tag']&1 if z['tag'] in [0x76,0x77] and z['tag']^z['inv']==255 else 1;assert value==want and sr==0x271f and sp==0xfffff900,('load/invalid fallback/ABI',z,value,sr,sp);out.append(z|dict(value=value))
  return out
 with ThreadPoolExecutor(max_workers=3) as pool:rows=sum(pool.map(trial,range((len(specs)+15)//16)),[])
 # Full byte-level storage separation: distinct sentinels across all four banks;
 # invoke actual put for each context, require only its own record/checksum move.
 checks=[]
 for slot in range(4):
  q=o/f'storage-put-{slot}';q.mkdir(exist_ok=True);raw=battery(1);b=bytearray(raw[1::2]);b[0x4d22:0x4d2a]=bytes.fromhex('619e609f619e609f');b[0x4d2a:0x4d32]=bytes.fromhex('718e708f718e708f');b[0x4d32:0x4d3a]=bytes.fromhex('738c728d738c728d');b[0x4d42:0x4d4a]=bytes.fromhex('7689768976897689');b[0x4d3a:0x4d42]=bytes.fromhex('758a748b758a748b');b=bytearray(pack(b)[1::2]);q.joinpath('before.srm').write_bytes(pack(b));physical=pack(b)
  body=[' move.b #1,0xa130f1',f' move.w #{int(slot>0)},0xfffff06c',f' move.w #{slot},0xfffff06a',' move.w #1,0xfffff810',' jsr 0x223014',' move.b #0,0xa130f1',' move.w 0xfffff810,0xffff4000']
  # run() writes its explicit battery fixture and reads back all64KB SRAM.
  q.joinpath('battery.srm').write_bytes(physical);run(q,'\n'.join(body),2);after=q.joinpath('battery.srm').read_bytes()[1::2];allowed={0x4d42+2*slot,0x4d43+2*slot,0x5ffe,0x5fff};diff=[i for i,(x,y) in enumerate(zip(b,after)) if x!=y];assert set(diff)<=allowed and after[0x4d42+2*slot:0x4d44+2*slot]==bytes.fromhex('7788'),('put overwrites another record/goalie/profile',slot,diff);assert after[0x5fff]==sum(after[0x4000:0x5ffe])%256 and after[0x5ffe]==after[0x5fff]^255,('Light checksum',slot);checks.append(dict(slot=slot,changed=diff))
 print('PASS',len(rows),'record validation/ABI contexts and four complete64KB separated writes',flush=True);return dict(loads=rows,writes=checks)

def menu_suite():
 p=o/'renderer';p.mkdir(exist_ok=True);c=asm(p,render_probe(""" cmpi.w #44,%d7
 bne.s original
 move.w %sr,-(%sp)
 addq.w #1,0xffffef90
 move.w 0xffffb028,0xffffef92
 move.l %a1,0xffffef94
 move.w (%sp)+,%sr
original: jmp 0x11ba4
""",a.rom,[44]),0x2fea00);b=bytearray(a.rom.read_bytes());assert set(b[0x2fea00:0x2fea00+len(c)])=={255} and b[0xf7bf4:0xf7bfa]==bytes.fromhex('4ef900011ba4');b[0x2fea00:0x2fea00+len(c)]=c;b[0xf7bf4:0xf7bfa]=bytes.fromhex('4ef9002fea00');observed=p/'rom.bin';observed.write_bytes(b);rows=[]
 def initial(gp=0,plans=(0,1,0,1),bad=None):
  b=bytearray(battery(gp)[1::2]);b[0x4d22:0x4d2a]=bytes.fromhex('619e619e619e619e');b[0x4d2a:0x4d32]=bytes.fromhex('718e718e718e718e');b[0xe20:0xe25]=b[0x2e20:0x2e25]=bytes.fromhex('123456789a')
  b[0x4d32:0x4d3a]=bytes.fromhex('738c738c738c738c')
  for slot,value in enumerate(plans):b[0x4d42+slot*2:0x4d44+slot*2]=bytes([0x76+value,0x89-value])
  b[0x4d3a:0x4d42]=bytes.fromhex('758a758a758a758a')
  if bad:b[0x4d44:0x4d46]=bytes.fromhex(bad)
  return pack(b)
 def menu(name,raw,commands,checks,dev=513,render=False,finish=False):
  for frame,expected in checks.items():
   if name.startswith('fresh-'):expected['charge']=0;expected['pickup']=0;expected['alt']=0
   elif name.startswith('restart-'):expected['charge']=0 if int(frame) in [3900,4600] else 1;expected['pickup']=expected['charge'];expected['alt']=expected['charge']
   else:expected['charge']=1;expected['pickup']=1;expected['alt']=1
  requested_menu_expectations(a.rom,name,checks)
  if name.startswith(('fresh-','corrupt-')) or name.startswith('restart-'):
   for frame,want in checks.items():
    if name.startswith(('fresh-','corrupt-')) or int(frame) in [3900,4600]:want['plan']=1
  p=o/name;p.mkdir(exist_ok=True);(p/'battery.srm').write_bytes(raw);(p/'spec.json').write_text(json.dumps(dict(name=name,checks=checks,render=render,finish=finish)));game_core(p,observed,commands,dict(GPGX_DEV=str(dev),GPGX_SRM=str(p/'battery.srm'),GPGX_HOOK=str(h/'testcases/shot_plan_menu_hook.py')));rows.append(dict(name=name,observations=json.loads((p/'results.json').read_text())));return(p/'battery.srm').read_bytes()
 down=[f'{900+50*i} press 1 Down 5' for i in range(down_count(a.rom,22))]
 for dev in [513,1281]:
  menu('fresh-'+str(dev),bytes(65536),down+['2250 snap','2401 exit'],{2200:dict(plan=0)},dev,True)
  raw=menu('choices-'+str(dev),initial(0,(0,0,0,0)),down+['2300 press 1 Right 5','2450 snap','2500 press 1 Left 5','2700 press 1 Right 5','2901 exit'],{2200:dict(plan=0,full_goalie='123456789a'),2400:dict(plan=1,plan_records='7689778876897689',record='8d72'),2600:dict(plan=0,plan_records='7689768976897689'),2800:dict(plan=1,record='8d72',full_goalie='123456789a')},dev,True)
  menu('reboot-'+str(dev),raw,['1301 exit'],{1200:dict(plan=1,plan_records='7689778876897689')},dev)
  nav=['1100 press 1 Right 5','1300 press 1 Right 5','1500 press 1 Right 5','1700 press 1 Left 5','1900 press 1 Left 5','2100 press 1 Left 5','2301 exit']
  raw=menu('slots-'+str(dev),initial(),nav,{1000:dict(plan=1,slot=1),1200:dict(plan=0,slot=2),1400:dict(plan=1,slot=3),1600:dict(plan=0,view=0),1800:dict(plan=1,slot=3),2000:dict(plan=0,slot=2),2200:dict(plan=1,slot=1)},dev)
  menu('slots-reboot-'+str(dev),raw,['1301 exit'],{1200:dict(plan=1,slot=1)},dev)
  menu('locked-'+str(dev),initial(1),down+['2300 press 1 Left 5','2500 press 1 Right 5','2701 exit'],{2200:dict(plan=1,gp=1),2400:dict(plan=1,gp=1),2600:dict(plan=1,gp=1)},dev,True)
  for bad in ['0000','7789','00ff','ffff']:
   menu('corrupt-'+bad+'-'+str(dev),initial(1,bad=bad),['1100 press 1 Right 5','1300 press 1 Left 5','1501 exit'],{1000:dict(plan=0,gp=1),1400:dict(plan=0,gp=1)},dev)
  native=[f'{1000+200*i} press 1 Right 5' for i in range(3)]+[f'{1800+40*i} press 1 Down 5' for i in range(native_down_count(a.rom,21))]
  raw=menu('native-'+str(dev),initial(1),native+['2900 press 1 Right 5','3101 exit'],{2850:dict(plan=0,view=0),3000:dict(plan=1,view=0,plan_records='7788778876897788',full_goalie='123456789a')},dev,True)
  menu('native-reboot-'+str(dev),raw,[f'{1000+200*i} press 1 Right 5' for i in range(3)]+['1701 exit'],{1600:dict(plan=1,view=0,full_goalie='123456789a')},dev)
  lines=['1100 press 1 Right 5','1300 press 1 Start 5']+[f'{1700+i*100} press 1 Down 5' for i in range(8)]+['2600 press 1 C 5','2900 press 1 Start 5','3200 press 1 C 5','3500 press 1 A 5','3500 press 1 C 5','4100 press 1 Left 5','4500 press 1 Right 5','4700 press 1 Right 5','5101 exit']
  menu('restart-'+str(dev),initial(1,(0,1,1,1)),lines,{3100:dict(plan=1,slot=2,gp=1),3900:dict(plan=0,slot=2,gp=0),4300:dict(plan=1,slot=1),4600:dict(plan=0,slot=2),4900:dict(plan=1,slot=3)},dev)
  raw=menu('first-game-'+str(dev),initial(),['1500 press 1 Start 5','1900 press 1 Start 5','16001 exit'],{1200:dict(plan=1),3300:dict(plan=1)},dev,False,dict(plan=1,gp=1))
  menu('completed-lock-'+str(dev),raw,down+['2300 press 1 Left 5','2601 exit'],{2200:dict(plan=1,gp=1),2500:dict(plan=1,gp=1)},dev,True)
 print('PASS',len(rows),'native menu/render/boot/slot/lock/restart/first-match cases',flush=True);return dict(cases=rows)


def observed_plan_image():
 p=o/'flow-observer';p.mkdir(exist_ok=True);code=asm(p,(h/'testcases/shot_plan_observer.s').read_text(),0x2d9000);b=bytearray(a.rom.read_bytes())
 assert b[0xe116:0xe120]==bytes.fromhex('6600000c08380000c2fa') and b[0xc314:0xc31a]==bytes.fromhex('31eb0052bed8') and b[0x2d9000:0x2d9000+len(code)]==b'\xff'*len(code)
 b[0x2d9000:0x2d9000+len(code)]=code;b[0xe116:0xe120]=bytes.fromhex('4ef9002d90004e714e71');b[0xc314:0xc31a]=bytes.fromhex('4ef9002d9200');assert b[0x1538a:0x15390]==bytes.fromhex('4ef9002efc00');b[0x1538a:0x15390]=bytes.fromhex('4ef9002d9400');(p/'rom.bin').write_bytes(b);return p/'rom.bin',code

def shot_observer_abi(code):
 payload=native_payload();cases=[dict(irq=i,ccr=c) for i in range(8) for c in range(32)];stub=bytes.fromhex('48f97fffffff4e0040f9ffff4e3c23cfffff4e3e4e75')
 def trial(batch,profile):
  ss=cases[batch*8:batch*8+8];body,obj=fixture(dict(team=0));body+=[' clr.w 0xffffef84']
  for j,z in enumerate(ss):
   init,regs=registers(a3=obj);body+=init+[f' move.w #{0x2000+(z["irq"]<<8)+z["ccr"]},%sr',' jsr 0xc314',' lea 0xffff4e00,%a0',f' lea 0x{0xffff4000+66*j:x},%a1',' moveq #32,%d0',f'snap{j}: move.w (%a0)+,(%a1)+',f' dbra %d0,snap{j}']
  body+=[' lea 0xffffb000,%a0',f' lea 0x{0xffff4000+66*len(ss):x},%a1',' move.w #0x4850/4-1,%d0','snap_ram: move.l (%a0)+,(%a1)+',' dbra %d0,snap_ram'];patches=[(0x230000,payload),(0xc31a,bytes.fromhex('4ef9002d4c00')),(0x2d4c00,stub)]
  if profile=='current':patches += [(0x2d9000,code),(0xc314,bytes.fromhex('4ef9002d9200'))]
  return run(o/f'shot-observer-{profile}-{batch}','\n'.join(body),66*len(ss)+len(payload),patches,profile)
 def pair(batch):
  before=trial(batch,'parent');after=trial(batch,'current');ss=cases[batch*8:batch*8+8];assert before[:66*len(ss)]==after[:66*len(ss)],('native shot observer full boundary ABI',batch)
  for j,z in enumerate(ss):
   c=abi(after[66*j:]);assert c['sp']==0xfffffdfc and c['sr']==0x2000+(z['irq']<<8)+(z['ccr']&16),('native displaced shot MOVE flags',z,c)
  old,new=bytearray(before[66*len(ss):]),bytearray(after[66*len(ss):]);assert int.from_bytes(new[0xef84-0xb000:0xef86-0xb000],'big')==8 and int.from_bytes(new[0xeb40-0xb000:0xeb42-0xb000],'big')==2,('positive native shot observer',batch)
  for lo,hi in [(0xeb40,0xeb50),(0xeb60,0xeb74),(0xef84,0xef86),(0xef88,0xef8a)]:old[lo-0xb000:hi-0xb000]=new[lo-0xb000:hi-0xb000]
  assert old==new,('shot observer changed actual game/RNG/plan state',batch);return ss
 with ThreadPoolExecutor(max_workers=3) as pool:rows=sum(pool.map(pair,range((len(cases)+7)//8)),[])
 return dict(cases=rows,full_native_GPR_SR_all_CCR_IRQ_SP_RAM_RNG_exact=True)


def target_observer_abi(code):
 payload=native_payload();cases=[dict(irq=i,ccr=c,flag=f) for i in range(8) for c in range(32) for f in [0,4]];stub=bytes.fromhex('48f97fffffff4e0040f9ffff4e3c23cfffff4e3e4e75')
 def trial(batch,observed):
  ss=cases[batch*8:batch*8+8];body,obj=fixture(dict(team=0))
  for j,z in enumerate(ss):
   body+=[' clr.l 0xffffef80',' move.l #0xa55a5aa5,0xfffff840',' move.l #0x5aa5a55a,0xfffff844',f' move.b #{z["flag"]},0xffffc2f2'];init,regs=registers(a3=obj);body+=init+[f' move.w #{0x2000+(z["irq"]<<8)+z["ccr"]},%sr',' jsr 0xe10c',' lea 0xffff4e00,%a0',f' lea 0x{0xffff4000+66*j:x},%a1',' moveq #32,%d0',f'snap{j}: move.w (%a0)+,(%a1)+',f' dbra %d0,snap{j}']
  body+=[' lea 0xffffb000,%a0',f' lea 0x{0xffff4000+66*len(ss):x},%a1',' move.w #0x4850/4-1,%d0','snap_ram: move.l (%a0)+,(%a1)+',' dbra %d0,snap_ram'];patches=[(0x230000,payload),(0xe120,bytes.fromhex('60000002')),(0xe124,bytes.fromhex('4ef9002d4c00')),(0x2d4c00,stub)]
  if observed:patches += [(0x2d9000,code),(0xe116,bytes.fromhex('4ef9002d90004e714e71'))]
  return run(o/f'target-observer-{int(observed)}-{batch}','\n'.join(body),66*len(ss)+len(payload),patches)
 def pair(batch):
  before=trial(batch,False);after=trial(batch,True);ss=cases[batch*8:batch*8+8];assert before[:66*len(ss)]==after[:66*len(ss)],('post-target observer full native boundary ABI',batch)
  for j,z in enumerate(ss):
   c=abi(after[66*j:]);assert c['sp']==0xfffffdfc and c['sr']==(0x2000+(z['irq']<<8)+(z['ccr']&~4)+(0 if z['flag'] else 4)),('displaced native branch/BTST flags',z,c)
  old,new=bytearray(before[66*len(ss):]),bytearray(after[66*len(ss):]);v=struct.unpack_from('>20H',new,0xeb00-0xb000);assert v[2]==v[3]==v[17]==2 and v[4]==4 and v[16]==0x4150 and v[:2]==(56,220),('positive actual route observer packet',batch,v)
  assert int.from_bytes(new[0xef82-0xb000:0xef84-0xb000],'big')==int(ss[-1]['flag']==0),('guarded observer counter path',batch)
  for lo,hi in [(0xeb00,0xeb28),(0xef80,0xef84)]:old[lo-0xb000:hi-0xb000]=new[lo-0xb000:hi-0xb000]
  assert old==new,('target observer changed native physics/plan state/RNG',batch);return ss
 with ThreadPoolExecutor(max_workers=3) as pool:rows=sum(pool.map(pair,range((len(cases)+7)//8)),[])
 return dict(cases=rows,full_native_GPR_SR_all_CCR_IRQ_SP_RAM_RNG_exact=True,positive_packet_per_batch=True)

def immediate_chains(r):
 return [dict(c,shot_frame=q['frame'],shot_serial=q['native_serial']) for c in r['catches'] for q in r['shots'] if c.get('native') and q['one_timer'] and c['target']==q['actor'] and c['sequence']==q['catch_sequence'] and c['serial']<q['native_serial'] and c['owner']==q['actor'] and q['native_passer']==c['actor'] and c['actor']!=c['target'] and c['actor']//6==c['target']//6 and 0<=q['frame']-c['catch_frame']<=2]

def flow_suite():
 assert a.flow_reference,'--flow-reference exact R7 required for matched native flow starts'
 observed,code=observed_plan_image();qualified=shot_observer_abi(code);target_qualified=target_observer_abi(code)
 from catch_observer_checks import verify as verify_catch_observer
 catch_qualified=verify_catch_observer(a.rom,code,h,o/'catch-observer-ABI')
 from shot_plan_start_fixture import capture
 def initial(on):
  b=bytearray(battery(0,((0,0,0,0),)*3,(0,0,0,0))[1::2]);b[0x4d42:0x4d4a]=bytes([0x76+on,0x89-on])*4;return pack(b)
 starts={on:capture(a.flow_reference,code,h,o/('genuine-start-'+str(on)),initial(on),on,observed)for on in[0,1]}
 cases=[dict(on=on,end=30000,cohort=cohort,profile='current')for cohort in['coldboot','matched-native-start']for on in[0,1]]+[dict(on=on,end=30000,cohort='matched-native-start',profile='reference')for on in[0,1]]
 def trial(z):
  p=o/f"flow-{z['cohort']}-{z['profile']}-{z['on']}";p.mkdir(exist_ok=True);(p/'battery.srm').write_bytes(initial(z['on']));(p/'spec.json').write_text(json.dumps(z));lines=['1500 press 1 Start 5','1900 press 1 Start 5'];settings=dict(GPGX_DEV='513',GPGX_SRM=str(p/'battery.srm'),GPGX_HOOK=str(h/'testcases/shot_plan_flow_hook.py'));image=observed
  if z['cohort']=='matched-native-start':
   start=starts[z['on']];lines.append('2500 load '+str(start['state'].resolve()));settings.update(GPGX_HOOK=str(start['hook']),COMMON_PAIRED_SRAM=str(start['paired']))
   if z['profile']=='reference':image=start['reference_observed']
  lines.append(f"{z['end']+1} exit");game_core(p,image,lines,settings);r=json.loads((p/'results.json').read_text());assert r['minimum_clock']<r['initial_clock'] and r['shots'] and r['passes'] and r['catches'],('natural native shot/pass/catch/clock coverage',z,{k:len(r[k])for k in['routes','shots','passes','catches']})
  if z['on']:
   routes=[x for x in r['routes']if x['new_route']];assert len(routes)>=24 and len({tuple(x['actor_xy'])for x in routes})>=3,('actual route/movement coverage',z)
   assert {tuple(map(abs,x['target']))for x in routes}=={(56,220),(28,248)} and {x['direction']for x in routes}=={-1,1} and {x['actor']//6 for x in r['shots']}=={0,1},('both actual route legs/ends and both-team native releases',z)
  else:assert not r['routes'],('Off ran optional routes',z)
  chains=immediate_chains(r)
  if z['on']and z['cohort']=='matched-native-start':assert chains,('actual native immediate pass-catch-shot chain missing from shared real start',z)
  r['native_immediate_pass_catch_shot_chains']=chains;r['setup_protocol']=('Genuine captured native start + exact paired SRAM restored at2500, then native AI; no individual action/RNG/outcome edits'if z['cohort']=='matched-native-start'else'Original unmodified cold-boot30k observation; native one-timer occurrence recorded, not assumed from startup timing')
  return r
 with ThreadPoolExecutor(max_workers=3)as pool:rows=list(pool.map(trial,cases))
 # Exercise chain-oracle rejections using actual positive native records.
 import copy
 positive=next(r for r in rows if r['spec']['on']and r['spec']['profile']=='current'and r['spec']['cohort']=='matched-native-start');chain=positive['native_immediate_pass_catch_shot_chains'][0];c=next(x for x in positive['catches']if x.get('native')and x['serial']==chain['serial']);q=next(x for x in positive['shots']if x['native_serial']==chain['shot_serial']);assert immediate_chains(dict(catches=[c],shots=[q]));rejected=[]
 for kind in['not-one-timer','wrong-passer','wrong-serial','beyond-two-frames']:
  bad=copy.deepcopy(q)
  if kind=='not-one-timer':bad['one_timer']=False
  elif kind=='wrong-passer':bad['native_passer']=bad['actor']
  elif kind=='wrong-serial':bad['native_serial']=c['serial']
  else:bad['frame']=c['catch_frame']+3
  assert not immediate_chains(dict(catches=[c],shots=[bad])),('chain negative escaped',kind);rejected.append(kind)
 print('PASS six native cold/shared-start flow cases; matched native immediate chains;256shot/512target/512catch observer ABI; four chain negatives',flush=True)
 return dict(cases=rows,genuine_start_provenance={str(on):start['provenance']for on,start in starts.items()},shot_observer_ABI=qualified,target_observer_ABI=target_qualified,catch_observer_ABI=catch_qualified,observer_sha256=sha(observed),chain_negatives_rejected=rejected,limitations=['Original cold-boot one-timer counts disclosed separately; matching genuine native starts isolates mechanics from menu startup timing.','Native functional coverage, not human difficulty, physical hardware or win-rate evidence.'])


def observed_save_image():
 p=o/'save-observer';p.mkdir(exist_ok=True);code=asm(p,(h/'testcases/alt_goalies_save_probe.s').read_text(),0x2d9000);data=bytearray(a.rom.read_bytes());assert data[0x2d9000:0x2d9000+len(code)]==b'\xff'*len(code);assert data[0x155fc:0x15604]==bytes.fromhex('4ef9002210084e71');data[0x2d9000:0x2d9000+len(code)]=code;data[0x155fc:0x15604]=bytes.fromhex('4ef9002d90004e71');path=p/'rom.bin';path.write_bytes(data);return path

def input_suite():
 observed=observed_save_image();b=bytearray(observed.read_bytes());p=o/'input-banks';p.mkdir(exist_ok=True);code=asm(p,(h/'testcases/manual_keeper_banks.s').read_text(),0x2ef400);assert b[0x2ef400:0x2ef400+len(code)]==b'\xff'*len(code);b[0x2ef400:0x2ef400+len(code)]=code
 for at,target,expected in [(0xad66,0x2ef400,'3f38c32831f8c32cc328'),(0xadce,0x2ef440,'31f8c320c32431dfc320'),(0xae00,0x2ef480,'3f38c32a31f8c32ec32a'),(0xae68,0x2ef4c0,'31f8c322c32631dfc322')]:
  assert b[at:at+10]==bytes.fromhex(expected);b[at:at+10]=bytes.fromhex('4ef9')+struct.pack('>I',target)+bytes.fromhex('4e714e71')
 observed=p/'rom.bin';observed.write_bytes(b);cases=[dict(count=count,adapter=count>2,mode=mode,cone=0,height=i%2,speed=(i//2)%2,charge=i%2,pickup=(i//2)%2,end=35000) for i,(count,mode) in enumerate([(1,0),(1,1),(2,0),(2,1),(3,0),(4,1)])]
 def trial(item):
  i,z=item;p=o/f'input-{i}';p.mkdir(exist_ok=True);b=bytearray(battery(0,((z['height'],z['speed'],z['mode'],z['cone']),)*3)[1::2])
  for off in [0,0x2000,0x4000,0x6000]:b[off+0x1ef2:off+0x1ef4]=bytes([z['count'],z['count']^255])
  for off in [0x4d22,0x4d24,0x4d26,0x4d28]:b[off:off+2]=bytes([0x60+z['charge'],0x9f-z['charge']])
  for off in [0x4d2a,0x4d2c,0x4d2e,0x4d30]:b[off:off+2]=bytes([0x70+z['pickup'],0x8f-z['pickup']])
  b[0x4d32:0x4d3a]=bytes.fromhex('738c738c738c738c');b[0x4d42:0x4d4a]=bytes.fromhex('7788778877887788');(p/'battery.srm').write_bytes(pack(b));(p/'spec.json').write_text(json.dumps(z));game_core(p,observed,['1500 press 1 Start 5','1900 press 1 Start 5','9000 snap',f'{z["end"]+1} exit'],dict(GPGX_DEV='1281' if z['adapter'] else '513',GPGX_SRM=str(p/'battery.srm'),GPGX_HOOK=str(h/'testcases/shot_plan_input_hook.py')));r=json.loads((p/'results.json').read_text());assert r['plan_on_whole_input_window'];assert set(r['shot_pads'])==set(range(z['count'])),('configured pads must all release actual C shots',z,r['shot_pads']);assert r['classified']>0 and r['outlets'],('natural actual saves and requested keeper outlets',z,r['classified'],r['outlets']);return r
 with ThreadPoolExecutor(max_workers=3) as pool:rows=list(pool.map(trial,enumerate(cases)))
 print('PASS',len(rows),'cold boot actual P1-P4 C shots, native saves and requested keeper outlets with independent pass settings',flush=True);return dict(cases=rows,method='Read-only host observations and real controller input after cold boot. Only observer counter initialized; no actor/puck/owner/score/RNG fixture writes. Scripted pads, not physical hardware tests.')

def live_starts():
 rows=[];source=(h/'testcases/shootout_start_hook.py').read_text();source=source.replace("        n=core.retro_serialize_size();buf=", "        assert rw(0xf046)==1 and rw(0xf820)==0x5355,('On shootout pool capture',rw(0xf046),raw(0xf820,32).hex())\n        n=core.retro_serialize_size();buf=")
 for side in [1,2]:
  q=o/('live-start-'+str(side));q.mkdir(exist_ok=True);b=bytearray(battery(0,((0,0,0,0),)*3,(0,0,0,0))[1::2]);b[0x4d3a:0x4d42]=bytes.fromhex('758a758a758a758a');b[0x4d42:0x4d4a]=bytes.fromhex('7788778877887788');(q/'battery.srm').write_bytes(pack(b));(q/'hook.py').write_text(source);case=dict(coop=False,side=side,force_at=1906,first_live_setup=True,teammates=2,period_choice=0);game_core(q,a.rom,['1500 press 1 Start 5','1900 press 1 Start 5','10000 exit'],dict(GPGX_DEV='513',GPGX_SRM=str(q/'battery.srm'),GPGX_HOOK=str(q/'hook.py'),SHOOTOUT_CASE=json.dumps(case)));events=json.loads((q/'events.json').read_text());first=next(x for x in events if x['event']=='first-live-no-input');assert first['active_buttons']==[] and first['controls'][0]==first['shooter'];rows.append(dict(side=side,state=str(q/'first-attempt.state'),first=first))
 return rows

def lifetime_suite():
 starts=live_starts();q=o/'lifetime-observer';q.mkdir(exist_ok=True);code=asm(q,""" move.w %sr,-(%sp)
 addq.w #1,0xffffef90
 move.w (%sp)+,%sr
 jmp 0x2f9000
 .org 0x40
leaders: move.w %sr,-(%sp)
 addq.w #1,0xffffef92
 move.w (%sp)+,%sr
 jmp 0x2fa000
""",0x2d9000);b=bytearray(a.rom.read_bytes())
 for old,new in [(0x2f9000,0x2d9000),(0x2fa000,0x2d9040)]:
  oldb=struct.pack('>I',old);assert b.count(oldb)>=7;b=b.replace(oldb,struct.pack('>I',new))
 assert b[0x2d9000:0x2d9000+len(code)]==b'\xff'*len(code);b[0x2d9000:0x2d9000+len(code)]=code;observed=q/'rom.bin';observed.write_bytes(b)
 cases=[dict(kind='ordinary',state=str(a.state.resolve()),down=i,canary=canary) for canary in [False,True] for i in [8,9,10,11,12,13]]+[dict(kind='shootout',state=x['state'],down=0) for x in starts]
 def trial(item):
  i,z=item;p=o/f'lifetime-{i}';p.mkdir(exist_ok=True);(p/'spec.json').write_text(json.dumps(z));(p/'battery.srm').write_bytes(battery(1));lines=[f'100 load {z["state"]}','160 press 1 Start 5']+[f'{400+40*j} press 1 Down 5' for j in range(z['down'])]+['1100 press 1 C 5','1300 snap','1500 press 1 Start 5','1750 snap','1800 press 1 Start 5','2101 exit'];game_core(p,observed,lines,dict(GPGX_DEV='513',GPGX_SRM=str(p/'battery.srm'),GPGX_HOOK=str(h/'testcases/shot_plan_lifetime_hook.py')));r=json.loads((p/'results.json').read_text());assert r['canaries_intact'] and r['paused_seen'] and r['resumed_seen'],('On memory lifetime/pause-resume',z,r);return r
 with ThreadPoolExecutor(max_workers=3) as pool:rows=list(pool.map(trial,enumerate(cases)))
 assert any(x['spec']['kind']=='ordinary' and x['views_seen'][0] for x in rows),'actual Season Standings not entered'
 assert any(x['spec']['kind']=='ordinary' and x['views_seen'][1] for x in rows),'actual Scoring Leaders not entered'
 print('PASS',len(rows),'On ordinary canaries/actual standings-leaders views and both-end real shootout metadata pause-resume lifetimes',flush=True);return dict(cases=rows)

sections={}
if a.section in ['all','geometry']:sections['geometry']=geometry_suite()
if a.section in ['all','decision']:sections['decision']=decision_suite()
if a.section in ['all','target']:sections['target']=target_suite()
if a.section in ['all','commit']:sections['commit']=commit_suite()
if a.section in ['all','storage']:sections['storage']=storage_suite()
if a.section in ['all','menu']:sections['menu']=menu_suite()
if a.section in ['all','flow']:sections['flow']=flow_suite()
if a.section in ['all','input']:sections['input']=input_suite()
if a.section in ['all','lifetime']:sections['lifetime']=lifetime_suite()
required={'geometry','decision','target','commit','flow','input','storage','menu','lifetime'} if a.section=='all' else {a.section}
assert set(sections)==required,('incomplete native shot-plan qualification',set(sections),required)
(o/'results.json').write_text(json.dumps(dict(rom_sha256=sha(a.rom),baseline_sha256=sha(a.baseline),sections=sections),indent=2)+'\n')
