#!/usr/bin/env python3
"""Fair co-op pickup: real native assignment, guarded ABI, collision and advancing RNG.

Fixture input specifies actors/controllers and (for finite enumeration only) a
random draw. Selected controllers, ownership flags and native RNG distributions
are never written as results. Original C0F8 executes in every assignment oracle.
"""
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
from itertools import combinations
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('--state',type=Path,required=True);ap.add_argument('--seed',type=Path,required=True);ap.add_argument('--section',choices=['all','assignment','abi','distribution','collision','runtime'],default='all');a=ap.parse_args();h=Path(__file__).resolve().parent;o=a.output.resolve();o.mkdir(parents=True,exist_ok=True);sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest();assert sha(a.baseline)=='40f388dc0beaedbd0883fdd4101a30debfb0fbbe62aab9d7a382e61476d513e0'
env={k:v for k,v in os.environ.items() if not k.startswith('GPGX_')}
def assemble(p,source,origin=0x2d5000):
 (p/'driver.s').write_text(source);subprocess.run([str(h.parent/'m68kasm.sh'),str(p/'driver.s'),f'{origin:x}',str(p/'code.bin')],check=True,stdout=subprocess.DEVNULL);return(p/'code.bin').read_bytes()
def core(p,rom,commands,settings=None):
 (p/'commands.txt').write_text(commands)
 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 or {}),stdout=log,stderr=log,check=True,timeout=120)
def run(p,profile,body,size,stub=False,stop=False,pad_oracle=False):
 c=assemble(p,' .text\n .globl _start\n_start:\n move.w #0x2700,%sr\n movea.l #0xfffff900,%sp\n'+body+'\n move.l #0x46414952,0xffff4ff0\nend: bra.s end\n');r=bytearray((a.baseline if profile in ['baseline','oracle'] else a.rom).read_bytes());assert len(c)<0x4000 and set(r[0x2d5000:0x2d5000+len(c)])=={255};r[0x2d5000:0x2d5000+len(c)]=c;struct.pack_into('>I',r,4,0x2d5000)
 if stub:r[0x11086:0x11096]=bytes.fromhex('70003039ffff3fe05279ffff3fe24e75')
 if stop:r[0x15390:0x15392]=bytes.fromhex('4e75')
 if pad_oracle:
  q=p/'oracle';q.mkdir(exist_ok=True);c2=assemble(q,""" .text
 .globl _start
_start:
 lea 0xffffc320,%a0
 move.w 0xffff3fe4,%d3
 move.w (%a0,%d3.w),%d1
 move.w 0x52(%a2),%d0
 jsr 0xc0f8
 lea 0xffffc320,%a0
 move.w %d0,(%a0,%d3.w)
 rts
""",0x2d9400);assert set(r[0x2d9400:0x2d9400+len(c2)])=={255};r[0x2d9400:0x2d9400+len(c2)]=c2;r[0x1538a:0x15390]=bytes.fromhex('4ef9002d9400')
 assert len(r)==len(a.rom.read_bytes()),'private instrumentation changed ROM length'
 (p/'rom.bin').write_bytes(r);core(p,p/'rom.bin',f'4000 dump ff4ff0 4 done\n4000 dump ff4000 {size} result\n4001 exit\n');d={l.split()[1]:bytes.fromhex(' '.join(l.split()[2:])) for l in (p/'commands.out').read_text().splitlines()};assert d['done']==b'FAIR',('native return',p);return d['result']
OBJ=0xffffb04a
def spec(team=0,pads=(0,1,2,3),adapter=True,**extra):
 base=6*team;s=dict(team=team,pads=list(pads),adapter=adapter,controls=[base+i if i in pads else -1 for i in range(4)],sides=[team+1 if i in pads else 0 for i in range(4)],carrier=base+4,locks=[],y=[],roles={},other_human_flags={},guard=None,draw=0);s.update(extra);return s

def setup(s,label):
 b=[' lea 0xffffb04a,%a0',' move.w #383,%d0',f'clear{label}: clr.l (%a0)+',f' dbra %d0,clear{label}']
 for i in range(12):b += [f' move.w #{i},0x{OBJ+128*i+0x52:x}',f' move.w #{0 if i%6==5 else 4},0x{OBJ+128*i+0x34:x}',f' move.b #{64 if i>=6 else 0},0x{OBJ+128*i+0x62:x}',f' move.b #30,0x{OBJ+128*i+0x71:x}']
 for pad,(actor,side) in enumerate(zip(s['controls'],s['sides'])):
  b += [f' move.w #{actor},0x{0xffffc320+pad*2:x}',f' move.w #{side},0x{0xffffc328+pad*2:x}']
  if 0<=actor<12 and side:b += [f' bset #3,0x{OBJ+128*actor+0x62:x}']
 for i,role in s['roles'].items():b += [f' move.w #{role},0x{OBJ+128*int(i)+0x34:x}']
 for i,v in s['other_human_flags'].items():b += [f' ori.b #{v},0x{OBJ+128*int(i)+0x64:x}']
 for pad in s['locks']:
  actor=s['controls'][pad]
  if 0<=actor<12:b += [f' bset #3,0x{OBJ+128*actor+0x63:x}']
 b += [' clr.l 0xfffff0b2',' clr.l 0xfffff0b6',' clr.l 0xfffff0e0',' clr.l 0xfffff0e4']
 for pad in s['y']:b += [f' move.b #-1,0x{(0xfffff0b2 if s["adapter"] else 0xfffff0e0)+pad*2:x}']
 b += [f' move.w #{int(s["adapter"])},0xffffd046',' move.w #-1,0xffffc318',' clr.w 0xffffd052',' clr.w 0xffffd412',' clr.w 0xffffc2ea',' clr.w 0xffffc2ec',' clr.w 0xffffc2ee',' clr.w 0xffffc2f0',' clr.w 0xffffc2f2',' clr.w 0xffffc2f6',' clr.w 0xffffc2fa',' clr.l 0xfffff064',f' move.w #{s.get("passer",-1)},0xffffbeda',f' move.w #{s.get("target",-1)},0xffffbee0',f' move.w #{s["carrier"]},0xffffb7aa',f' move.w #{s["draw"]},0xffff3fe0',' clr.w 0xffff3fe2',' move.l #0x12345678,0xffffd066']
 guards={'shootout':('0xc2fa',1),'penalty':('0xc2f2',4),'whistle':('0xc2ea',1),'stoppage':('0xc2ea',16),'pause':('0xd412',1)}
 if s['guard'] in guards:
  at,v=guards[s['guard']];op='w' if s['guard']=='pause' else 'b';b += [f' move.{op} #{v},0x{0xffff0000+int(at,16):x}']
 if s['guard']=='human':b += [f' bset #3,0x{OBJ+128*s["carrier"]+0x62:x}']
 b += [f' moveq #{s["carrier"]},%d0',f' lea 0x{OBJ+128*s["carrier"]:x},%a2']
 return b

def snapshot(dest):
 b=[f' move.w %sr,0x{dest:x}',f' movem.l %d0-%d7/%a0-%a6,0x{dest+2:x}',f' move.l %sp,0x{dest+62:x}']
 for i in range(4):b += [f' move.l 0x{0xffffc320+4*i:x},0x{dest+66+4*i:x}']
 for i in range(12):
  for k in range(3):b += [f' move.b 0x{OBJ+128*i+0x62+k:x},0x{dest+82+3*i+k:x}']
 b += [f' move.w 0xffffb7aa,0x{dest+118:x}',f' move.l 0xffffd066,0x{dest+120:x}',f' move.w 0xffff3fe2,0x{dest+124:x}'];return b

def decode(raw):return dict(sr=int.from_bytes(raw[:2],'big'),registers=list(struct.unpack('>15I',raw[2:62])),sp=int.from_bytes(raw[62:66],'big'),controls=list(struct.unpack('>4h',raw[66:74])),sides=list(struct.unpack('>4H',raw[74:82])),flags=[list(raw[82+3*i:85+3*i]) for i in range(12)],owner=int.from_bytes(raw[118:120],'big'),rng=int.from_bytes(raw[120:124],'big'),draw_calls=int.from_bytes(raw[124:126],'big'))

def eligible(s):
 count=4 if s['adapter'] else 2;side=s['team']+1;friendly=[i for i in range(count) if s['sides'][i]==side];out=[]
 for pad in friendly:
  actor=s['controls'][pad]
  if pad in s['y']:continue
  if actor<0:out.append(pad);continue
  if actor>11 or actor//6!=s['team'] or s['roles'].get(actor,0 if actor%6==5 else 4)<=0 or pad in s['locks']:continue
  if any(i!=pad and s['sides'][i] and s['controls'][i]==actor for i in range(count)):continue
  out.append(pad)
 owned=any(actor==s['carrier'] and side for actor,side in zip(s['controls'],s['sides']))
 return out if len(friendly)>=2 and not s['guard'] and s['carrier']%6!=5 and not owned else []

# Enumerate every eligible draw over actual physical subsets and safety exclusions.
cases=[]
for team in [0,1]:
 for count in [2,3,4]:
  for pads in combinations(range(4),count):
   for draw in range(count):cases.append(spec(team,pads,draw=draw))
 for draw in range(2):cases.append(spec(team,(0,1),False,draw=draw))
 for pads in combinations(range(4),2):
  b=spec(team,pads);other=[p for p in range(4) if p not in pads]
  for j,pad in enumerate(other):b['sides'][pad]=2-team;b['controls'][pad]=6*(1-team)+j
  for draw in range(2):cases.append(dict(b,draw=draw))
 for block in ['pass-lock','Y','keeper','inactive','invalid','other-side','unassigned','duplicate','native-AI-bit']:
  for pad in range(4):
   b=spec(team);actor=b['controls'][pad]
   if block=='pass-lock':b['locks']=[pad]
   if block=='Y':b['y']=[pad]
   if block=='keeper':b['controls'][pad]=team*6+5
   if block=='inactive':b['roles']={actor:-1}
   if block=='invalid':b['controls'][pad]=12
   if block=='other-side':b['controls'][pad]=6*(1-team)
   if block=='unassigned':b['controls'][pad]=-1
   if block=='duplicate':b['controls'][pad]=b['controls'][(pad+1)%4]
   if block=='native-AI-bit':b['other_human_flags']={actor:8}
   for draw in range(len(eligible(b))):cases.append(dict(b,draw=draw,reason=block))
 # Only one eligible pad: a pending/locked/manual keeper must not block another.
 for survivor in range(4):cases.append(spec(team,locks=[i for i in range(4) if i!=survivor],reason='one-eligible'))
guard_cases=[]
for team in [0,1]:
 guard_cases += [spec(team,guard=g) for g in ['shootout','penalty','whistle','stoppage','pause','human']]
 guard_cases += [spec(team,pads=(0,)),spec(team,pads=(1,)),spec(team,pads=()),spec(team,locks=[0,1,2,3]),spec(team,y=[0,1,2,3]),spec(team,pads=(2,3),adapter=False)]
 g=spec(team,carrier=team*6+5);guard_cases.append(g)
 for pad in range(4):
  b=spec(team);b['carrier']=b['controls'][pad];guard_cases.append(b)
 b=spec(team,pads=(0,));b['sides'][1]=2-team;b['controls'][1]=6*(1-team);guard_cases.append(b)

def assignments(item):
 profile,batch=item;ss=cases[batch*8:batch*8+8];p=o/f'assign-{profile}-{batch:03}';p.mkdir(exist_ok=True);body=[]
 for j,s in enumerate(ss):
  body += setup(s,str(j))
  if profile=='oracle':
   pad=eligible(s)[s['draw']];body += [f' move.w #{s["controls"][pad]},%d1',f' moveq #{s["carrier"]},%d0',' jsr 0xc0f8',f' move.w %d0,0x{0xffffc320+pad*2:x}']
  else:body += [' jsr 0x15322']
  body += snapshot(0xffff4000+j*128)
 raw=run(p,profile,'\n'.join(body),128*len(ss),True);return profile,batch,[decode(raw[j*128:(j+1)*128]) for j in range(len(ss))]
def assignment_suite():
 with ThreadPoolExecutor(max_workers=3) as pool:assigned=list(pool.map(assignments,[(v,b) for b in range((len(cases)+7)//8) for v in ['baseline','current','oracle']]))
 lookup={(v,b):rows for v,b,rows in assigned};assignment_rows=[]
 for i,s in enumerate(cases):
  b,j=divmod(i,8);old=lookup['baseline',b][j];new=lookup['current',b][j];oracle=lookup['oracle',b][j];selected=eligible(s)[s['draw']]
  for key in ['controls','sides','flags','owner']:assert new[key]==oracle[key],('native physical assignment oracle',s,selected,key,new,oracle)
  assert new['sp']==0xfffff900 and new['owner']==s['carrier'] and new['draw_calls']==int(len(eligible(s))>1),('assignment/stack/exactly-one-draw',s,new)
  assert new['controls'][selected]==s['carrier'] and all(new['controls'][pad]==s['controls'][pad] for pad in range(4) if pad!=selected),('other pad stolen',s,new)
  assignment_rows.append(dict(spec=s,selected_pad=selected,baseline_controls=old['controls'],current_controls=new['controls'],native_oracle=True,draw_calls=new['draw_calls']))
 # Full native entry guards preserve the parent's complete observation, not just its result.
 def guards(item):
  profile,batch=item;ss=guard_cases[batch*8:batch*8+8];p=o/f'guard-{profile}-{batch:03}';p.mkdir(exist_ok=True);body=[]
  for j,s in enumerate(ss):body += setup(s,str(j))+[' jsr 0x15322']+snapshot(0xffff4000+j*128)
  raw=run(p,profile,'\n'.join(body),128*len(ss),True);return profile,batch,[raw[j*128:(j+1)*128] for j in range(len(ss))]
 with ThreadPoolExecutor(max_workers=3) as pool:guard_rows=list(pool.map(guards,[(v,b) for b in range((len(guard_cases)+7)//8) for v in ['baseline','current']]))
 glookup={(v,b):rows for v,b,rows in guard_rows}
 for i,s in enumerate(guard_cases):
  b,j=divmod(i,8);assert glookup['baseline',b][j]==glookup['current',b][j],('guard full native state/ABI',s,decode(glookup['baseline',b][j]),decode(glookup['current',b][j]))
 report=dict(assignment_contexts=len(cases),unchanged_guard_contexts=len(guard_cases),assignments=assignment_rows);print('PASS',len(cases),'native physical-pad/eligibility/draw/oracle contexts;',len(guard_cases),'unchanged native guards',flush=True);return report

# Direct installed hook ABI: fallback matches displaced native instructions;
# handled arbitration preserves all entry registers/SR with balanced stack.
abi_specs=[dict(s,ccr=ccr) for s in guard_cases for ccr in [0,31]]
for team in [0,1]:
 for count in [2,3,4]:
  for ccr in [0,31]:abi_specs.append(spec(team,tuple(range(count)),ccr=ccr))
 for idx in [0,5,6,11,12,0x8000,65535]:
  for ccr in [0,31]:abi_specs.append(spec(team,guard='index',entry_index=idx,ccr=ccr))
def abi_suite():
 def trial(item):
  profile,batch=item;ss=abi_specs[batch*8:batch*8+8];p=o/f'abi-{profile}-{batch:03}';p.mkdir(exist_ok=True);body=[]
  for j,z in enumerate(ss):
   body += setup(z,str(j));ds=[0x11223300+i for i in range(8)];ds[0]=(ds[0]&0xffff0000)|(z.get('entry_index',z['carrier'])&65535);ars=[0xffff1800+256*i for i in range(7)];ars[2]=OBJ+128*z['carrier']
   body += [f' move.l #0x{v:x},%d{i}' for i,v in enumerate(ds)]+[f' movea.l #0x{v:x},%a{i}' for i,v in enumerate(ars)]+[f' move.w #{0x2700|z["ccr"]},%sr',' jsr 0x1538a']+snapshot(0xffff4000+j*128)
  raw=run(p,profile,'\n'.join(body),128*len(ss),True,True);return profile,batch,[decode(raw[j*128:(j+1)*128]) for j in range(len(ss))]
 with ThreadPoolExecutor(max_workers=3) as pool:rows=list(pool.map(trial,[(v,b) for b in range((len(abi_specs)+7)//8) for v in ['baseline','current']]))
 lookup={(v,b):r for v,b,r in rows};handled=0
 for i,z in enumerate(abi_specs):
  b,j=divmod(i,8);new=lookup['current',b][j];old=lookup['baseline',b][j]
  if eligible(z):
   ds=[0x11223300+i for i in range(8)];ds[0]=(ds[0]&0xffff0000)|z['carrier'];ars=[0xffff1800+256*i for i in range(7)];ars[2]=OBJ+128*z['carrier'];assert new['registers']==ds+ars and new['sr']==0x2700|z['ccr'] and new['sp']==0xfffff900,('handled hook ABI',z,new);handled+=1
  else:
   for key in ['registers','sr','sp','controls','sides','flags','owner','rng','draw_calls']:assert new[key]==old[key],('fallback native CMP/SLT ABI',z,key,new,old)
 print('PASS',len(abi_specs),'paired direct hook ABI contexts;',handled,'handled entry-state contracts',flush=True);return dict(paired_contexts=len(abi_specs),handled=handled,fallback=len(abi_specs)-handled)

# Real advancing native RNG: no random samples or selected controllers are injected.
distribution_specs=[dict(team=team,count=count,adapter=adapter,seed=seed) for count,adapter in [(2,False),(2,True),(3,True),(4,True)] for team in [0,1] for seed in [0,0x12345678,0xdeadbeef]]
def distribution_suite():
 draws=6000
 def trial(item):
  profile,batch=item;ss=distribution_specs[batch*4:batch*4+4];p=o/f'distribution-{profile}-{batch}';p.mkdir(exist_ok=True);body=[]
  for j,z in enumerate(ss):
   g=spec(z['team'],tuple(range(z['count'])),z['adapter']);base=6*z['team'];dest=0xffff4000+12*j;body += setup(g,f'd{j}')+[f' move.l #0x{z["seed"]:x},0xffffd066',f' lea 0x{dest:x},%a6',' clr.l (%a6)',' clr.l 4(%a6)',f' move.w #{draws-1},%d6',f'sample{j}:']
   if profile=='oracle':body += [f' moveq #{z["count"]},%d0',' jsr 0x11086',' add.w %d0,%d0',' addq.w #1,(%a6,%d0.w)']
   else:
    for pad in range(4):
     actor=base+pad if pad<z['count'] else -1;body += [f' move.w #{actor},0x{0xffffc320+pad*2:x}']
     if pad<z['count']:body += [f' move.b #{64*z["team"]+8},0x{OBJ+128*actor+0x62:x}']
    body += [f' move.b #{64*z["team"]},0x{OBJ+128*g["carrier"]+0x62:x}',f' move.w #{g["carrier"]},0xffffb7aa',f' moveq #{g["carrier"]},%d0',f' lea 0x{OBJ+128*g["carrier"]:x},%a2',' jsr 0x15322',' lea 0xffffc320,%a0',' moveq #0,%d2',f'find{j}:',f' cmpi.w #{g["carrier"]},(%a0,%d2.w)',f' beq.s found{j}',' addq.w #2,%d2',' cmpi.w #8,%d2',f' bcs.s find{j}',' move.l #0x42414421,0xffff4ff0',f'bad{j}: bra.s bad{j}',f'found{j}: addq.w #1,(%a6,%d2.w)']
   body += [f' dbra %d6,sample{j}',f' move.l 0xffffd066,0x{dest+8:x}']
  raw=run(p,profile,'\n'.join(body),12*len(ss));return profile,batch,[dict(counts=list(struct.unpack_from('>4H',raw,12*j)),rng=int.from_bytes(raw[12*j+8:12*j+12],'big')) for j in range(len(ss))]
 with ThreadPoolExecutor(max_workers=3) as pool:rows=list(pool.map(trial,[(v,b) for b in range((len(distribution_specs)+3)//4) for v in ['baseline','current','oracle']]))
 lookup={(v,b):r for v,b,r in rows};report=[]
 for i,z in enumerate(distribution_specs):
  b,j=divmod(i,4);old=lookup['baseline',b][j];new=lookup['current',b][j];control=lookup['oracle',b][j];assert old['counts']==[draws,0,0,0] and old['rng']==z['seed'],('parent first-pad bias',z,old);assert new==control,('advancing native RNG control/exact draw count',z,new,control);assert sum(new['counts'])==draws and all(abs(v-draws/z['count'])<=0.1*draws/z['count'] for v in new['counts'][:z['count']]) and not any(new['counts'][z['count']:]),('physical distribution',z,new)
  report.append(dict(spec=z,baseline=old,current=new,exact_to_native_rng_only_control=True))
 print('PASS',len(report),'advancing-RNG series;',len(report)*draws,'native pickup draws per current/control profile',flush=True);return dict(series=len(report),draws_per_series=draws,current_native_pickup_draws=len(report)*draws,baseline_bias_reproduced=True,exact_native_rng_control=True,results=report)

# Full native stick/body collision, real GetHot and accepted possession/controller path.
collision_specs=[]
for team in [0,1]:
 for count,adapter in [(2,False),(2,True),(3,True),(4,True)]:
  for draw in range(count):collision_specs.append(spec(team,tuple(range(count)),adapter,draw=draw))
 for pad in range(4):collision_specs.append(spec(team,carrier=team*6+pad,passer=team*6+5,target=team*6+pad,reason='explicit-human-receiver'))
 collision_specs.append(spec(team,carrier=team*6+5,reason='keeper'))
def collision_suite():
 def trial(item):
  profile,batch=item;ss=collision_specs[batch*4:batch*4+4];p=o/f'collision-{profile}-{batch:02}';p.mkdir(exist_ok=True);body=[]
  for j,z in enumerate(ss):
   body += setup(z,str(j))+[' move.w #-1,0xffffb7aa',' clr.l 0xfffff000',' clr.w 0xfffff736',' clr.w 0xffffb762',' clr.w 0xffffb772',' clr.w 0xffffb774',' clr.w 0xffffb776',' clr.w 0xffffb7a8',' move.l %a2,-(%sp)',' jsr 0x106e0',' move.w %d0,0xffffb74a',' move.w %d1,0xffffb75e',' lea 0xffffb74a,%a3',f' move.w #{z["carrier"]*2},%d4']
   selected=eligible(z)[z['draw']] if eligible(z) else 0;body += [f' move.w #{selected*2},0xffff3fe4',' jsr 0x14e10']+snapshot(0xffff4000+j*128)
  raw=run(p,profile,'\n'.join(body),128*len(ss),True,pad_oracle=profile=='oracle');return profile,batch,[decode(raw[j*128:(j+1)*128]) for j in range(len(ss))]
 with ThreadPoolExecutor(max_workers=3) as pool:rows=list(pool.map(trial,[(v,b) for b in range((len(collision_specs)+3)//4) for v in ['baseline','current','oracle']]))
 lookup={(v,b):r for v,b,r in rows};report=[]
 for i,z in enumerate(collision_specs):
  b,j=divmod(i,4);old=lookup['baseline',b][j];new=lookup['current',b][j];oracle=lookup['oracle',b][j]
  for key in ['controls','sides','flags','owner']:assert new[key]==oracle[key],('full native collision/assignment oracle',z,key,new,oracle)
  assert new['owner']==z['carrier'] and new['sp']==0xfffff900,('actual native pickup/stack',z,new)
  if z.get('reason') in ['explicit-human-receiver','keeper']:
   for key in ['controls','sides','flags','owner','rng','draw_calls']:assert new[key]==old[key],('explicit receiver/keeper changed',z,key,new,old)
  report.append(dict(spec=z,baseline_controls=old['controls'],current_controls=new['controls'],owner=new['owner'],native_pickup=True,native_oracle=True))
 print('PASS',len(report),'full native GetHot/stick/body pickup/controller contexts',flush=True);return dict(contexts=len(report),cases=report)

def runtime_suite():
 p=o/'runtime-probes';p.mkdir(exist_ok=True)
 bank=assemble(p,(h/'testcases/manual_keeper_banks.s').read_text(),0x2ef400)
 q=p/'native';q.mkdir(exist_ok=True)
 code=assemble(q,""" .text
 .globl _start
_start:
 move.w %sr,-(%sp)
 movem.l %d0-%d7/%a0-%a6,-(%sp)
 move.w %d5,0xffffefa0
 move.w %d0,0xffffefa2
 move.w %d1,0xffffefa4
 move.w %d7,0xffffefa6
 move.w %d6,0xffffefa8
 move.l 0xffffc320,0xffffefaa
 move.l 0xffffc324,0xffffefae
 move.l 0xffffc328,0xffffefb2
 move.l 0xffffc32c,0xffffefb6
 move.w 0xfffff0d4,0xffffefba
 addq.w #1,0xffffef80
 movem.l (%sp)+,%d0-%d7/%a0-%a6
 move.w (%sp)+,%sr
 jmp 0xc0f8
 .org 0x100
 jsr 0xfd78a
 tst.w 0xffffef90
 beq.w done
 move.w %sr,-(%sp)
 movem.l %d0-%d7/%a0-%a6,-(%sp)
 clr.w 0xffffef90
 tst.w 0xffffb7aa
 bpl.s restore
 move.w 0xffffef92,%d0
 lsl.w #7,%d0
 lea 0xffffb04a,%a2
 adda.w %d0,%a2
 move.l %a2,-(%sp)
 jsr 0x106e0
 add.w (%a2),%d0
 add.w 0x14(%a2),%d1
 move.w %d0,0xffffb74a
 move.w %d1,0xffffb75e
 clr.w 0xffffb762
 clr.w 0xffffb772
 clr.w 0xffffb774
 clr.w 0xffffb776
 move.w #1,0xffffef94
 lea 0xffffb74a,%a3
 move.w 0xffffef92,%d4
 add.w %d4,%d4
 jsr 0x14e10
restore:
 movem.l (%sp)+,%d0-%d7/%a0-%a6
 move.w (%sp)+,%sr
done: rts
""",0x2d9000)
 r=bytearray(a.rom.read_bytes())
 for at,c in [(0x2ef400,bank),(0x2d9000,code)]:
  assert set(r[at:at+len(c)])=={255};r[at:at+len(c)]=c
 for at,target,expected in [(0xad66,0x2ef400,'3f38c32831f8c32cc328'),(0xadce,0x2ef440,'31f8c320c32431dfc320'),(0xae00,0x2ef480,'3f38c32a31f8c32ec32a'),(0xae68,0x2ef4c0,'31f8c322c32631dfc322')]:
  assert r[at:at+10]==bytes.fromhex(expected);r[at:at+10]=bytes.fromhex('4ef9')+struct.pack('>I',target)+bytes.fromhex('4e714e71')
 for start,end,old,new in [(0x2efc00,0x2f0000,'4eb90000c0f8','4eb9002d9000'),(0x2e1000,0x2e1200,'4eb9000fd78a','4eb9002d9100')]:
  match=bytes(r[start:end]);assert match.count(bytes.fromhex(old))==1;at=start+match.index(bytes.fromhex(old));r[at:at+6]=bytes.fromhex(new)
 (p/'rom.bin').write_bytes(r)
 specs=[dict(team=team,count=count,adapter=adapter,seed=(i*0x9e3779b9)&0xffffffff) for count,adapter in [(2,False),(3,True),(4,True)] for team in [0,1] for i in range(8)]
 def trial(item):
  i,s=item;q=o/f'runtime-{i:03}';q.mkdir(exist_ok=True);(q/'spec.json').write_text(json.dumps(s));core(q,p/'rom.bin',f'100 load {(a.state if s["adapter"] else a.state.parent.parent/"goalie-outlet-20261024-173340/capture"/f"gk{6*s['team']+5}-clear.state").resolve()}\n601 exit\n',dict(GPGX_DEV='1281' if s['adapter'] else '513',GPGX_HOOK=str(h/'testcases/fair_pickup_input_hook.py')));return json.loads((q/'results.json').read_text())
 with ThreadPoolExecutor(max_workers=3) as pool:results=list(pool.map(trial,enumerate(specs)))
 for count in [2,3,4]:
  winners={r['winner'] for r in results if r['spec']['count']==count};assert winners==set(range(count)),('unexercised physical winner',count,winners)
 print('PASS',len(results),'native loose-puck/RNG/physical-pad movement branches',flush=True);return dict(contexts=len(results),scope='Declared loose-puck game-state branches; native ownership, real evolving RNG and physical input; not a physical-controller playtest or unforced match.',cases=results)

sections={}
if a.section in ['all','assignment']:sections['assignment']=assignment_suite()
if a.section in ['all','abi']:sections['abi']=abi_suite()
if a.section in ['all','distribution']:sections['distribution']=distribution_suite()
if a.section in ['all','collision']:sections['collision']=collision_suite()
if a.section in ['all','runtime']:sections['runtime']=runtime_suite()
if a.section=='all':assert set(sections)=={'assignment','abi','distribution','collision','runtime'}
(o/'results.json').write_text(json.dumps(dict(rom_sha256=sha(a.rom),baseline_sha256=sha(a.baseline),section=a.section,complete=a.section=='all',sections=sections),indent=2)+'\n')
