#!/usr/bin/env python3
"""Optional painted-dot policy: native geometry, exact boundary ABI and saves.
Declared CPU/input fixtures and image controls are not human or win-rate trials.
"""
from pathlib import Path
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','commit','storage','menu','flow','lifetime','observer'],default='all');a=ap.parse_args();h=Path(__file__).resolve().parent;o=a.output.resolve();o.mkdir(parents=True,exist_ok=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)=='0b4b104cf31e3b78d5f686349bbaad73ed54ba491c23c6ac661aca02eca396ca'

def asm(p,body,origin=0x2d5000):
 p.mkdir(parents=True,exist_ok=True);(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 core(p,rom,commands,settings=None):
 (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 or {}),stdout=log,stderr=log,check=True,timeout=120)
def run(p,body,size,patches=(),profile='current'):
 p.mkdir(parents=True,exist_ok=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 #0x46444f54,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 patch in patches:
  at,data=patch[:2]
  expected=patch[2] if len(patch)==3 else bytes.fromhex('08f80002c2ea') if at==0x12562 else b'\xff'*len(data)
  assert b[at:at+len(data)]==expected,(hex(at),'instrumentation reservation');b[at:at+len(data)]=data
 (p/'rom.bin').write_bytes(b);core(p,p/'rom.bin',['1500 dump ff3ff0 4 done',f'1500 dump ff4000 {size} result','1501 exit'],{'GPGX_SRM':str(p/'battery.srm')} if (p/'battery.srm').exists() else None)
 rows={t[1]:bytes.fromhex(' '.join(t[2:])) for line in(p/'commands.out').read_text().splitlines() if(t:=line.split())};assert rows['done']==b'FDOT',('native return',p);return rows['result']
def oracle(x,y):
 if not(-70<=x<=70 and -206<=y<=206):return x,y
 points=[(70,206),(-70,206)] if y>=166 else [(70,-206),(-70,-206)] if y<=-166 else [(0,0),(70,65),(-70,65),(70,-65),(-70,-65)]
 return min(points,key=lambda p:(p[0]-x)**2+(p[1]-y)**2)
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(x,y):
 regs=[0x22330000+i for i in range(8)]+[0xffff1000+256*i for i in range(7)];regs[0]=0x12340000|(x&65535);regs[1]=0x56780000|(y&65535)
 return [f' move.l #0x{v:x},%d{i}' for i,v in enumerate(regs[:8])]+[f' movea.l #0x{v:x},%a{i}' for i,v in enumerate(regs[8:])],regs
CANARY=bytes((i*37+(i>>8)*11+19)&255 for i in range(0x4850))
def copy_native():return [' lea 0x22F000,%a0',' lea 0xffffb000,%a1',' move.w #0x4850/4-1,%d0','copy: move.l (%a0)+,(%a1)+',' dbra %d0,copy']
def capture_ram(at):return[' lea 0xffffb000,%a0',f' lea 0x{at:x},%a1',' move.w #0x4850/4-1,%d0','capture: move.l (%a0)+,(%a1)+',' dbra %d0,capture']
def geometry_suite():
 points=[(x,y) for x in range(-70,71) for y in range(-206,207)];points +=[(x,y) for x in [-32768,-71,-70,0,70,71,32767] for y in [-32768,-207,-206,0,206,207,32767] if not(-70<=x<=70 and -206<=y<=206)];batches=[points[i:i+4096] for i in range(0,len(points),4096)]
 def exhaustive(item):
  index,ss=item;data=b''.join(struct.pack('>hh',*p) for p in ss);body=[' lea 0x22F000,%a0',' lea 0xffff4000,%a1',' move.l #0x12340000,%d0',' move.l #0x56780000,%d1',f' move.w #{len(ss)-1},%d7','each: move.w (%a0)+,%d0',' move.w (%a0)+,%d1',' jsr 0x224004',' move.l %d0,(%a1)+',' move.l %d1,(%a1)+',' dbra %d7,each'];raw=run(o/f'geometry-grid-{index}','\n'.join(body),8*len(ss),[(0x22F000,data)])
  for j,p in enumerate(ss):
   x,y=oracle(*p);actual=struct.unpack_from('>II',raw,j*8);assert actual==(0x12340000|(x&65535),0x56780000|(y&65535)),('native squared-distance/region/tie or upperword',p,actual,(x,y))
  return dict(cases=len(ss),input_sha256=hashlib.sha256(data).hexdigest(),output_sha256=hashlib.sha256(raw).hexdigest())
 with ThreadPoolExecutor(max_workers=3) as pool:grids=list(pool.map(exhaustive,enumerate(batches)))
 specs=[dict(irq=irq,ccr=ccr,point=[(-14,-12),(0,0),(0,165),(70,-206),(71,0),(-70,166)][(irq*32+ccr)%6]) for irq in range(8) for ccr in range(32)]
 def contexts(batch):
  ss=specs[batch*16:batch*16+16];body=copy_native()
  for j,z in enumerate(ss):
   init,regs=registers(*z['point']);body+=init+[f' move.w #{0x2000+(z["irq"]<<8)+z["ccr"]},%sr',' jsr 0x224004']+snapshot(0xffff4000+66*j)
  body+=capture_ram(0xffff4000+66*len(ss));raw=run(o/f'geometry-ABI-{batch}','\n'.join(body),66*len(ss)+len(CANARY),[(0x22F000,CANARY)])
  for j,z in enumerate(ss):
   _,regs=registers(*z['point']);x,y=oracle(*z['point']);regs[0]=0x12340000|(x&65535);regs[1]=0x56780000|(y&65535);ctx=abi(raw[66*j:]);assert ctx==dict(regs=regs,sr=0x2000+(z['irq']<<8)+z['ccr'],sp=0xfffff900),('geometry complete ABI',z,ctx)
  assert raw[66*len(ss):]==CANARY,('geometry touched game RAM/RNG',batch);return dict(contexts=ss,complete_active_RAM_after_batch_exact=True)
 with ThreadPoolExecutor(max_workers=3) as pool:contracts=list(pool.map(contexts,range(16)))
 print('PASS',len(points),'native coordinate pairs and256full CCR/IRQ/GPR/SP geometry contexts',flush=True);return dict(valid_coordinate_pairs=58233,invalid_coordinate_pairs=len(points)-58233,batches=grids,ABI_batches=contracts,rule='same native region, squared nearest, documented deterministic ties; all9canonical dots unchanged')
def commit_suite():
 specs=[dict(irq=irq,ccr=ccr,option=option,point=[(-14,-12),(20,40),(70,206),(-70,-206),(0,100),(0,-100)][(irq*32+ccr)%6],side=(irq+ccr)%2) for option in [0,1,99] for irq in range(8) for ccr in range(32)]
 specs += [dict(irq=i%8,ccr=(i*7)%32,option=1,point=(-14,-12),side=i%2,guard=g) for i,g in enumerate(['native-so','native-flight','native-both','countdown-one','countdown-sixty','countdown-invalid','ot-so','ot-later','ot-other','ot-invalid'])]
 stub=bytes.fromhex('48f97fffffff4e0040f9ffff4e3c23cfffff4e3e4e75')
 def trial(batch,profile):
  ss=specs[batch*16:batch*16+16];body=copy_native()
  for j,z in enumerate(ss):
   x,y=z['point'];body += [f' move.w #{x},0xffffbf94',f' move.w #{y},0xffffbf96',' move.w #0x1802,0xffffc3a4',' clr.w 0xffffc3a6',f' move.b #{128*z["side"]},0xffffb1ac',f' move.w #{z["option"]},0xfffff848',' move.w #0x1234,0xffffc3e6',' clr.w 0xfffff090',' clr.w 0xffffd412',' clr.b 0xffffc2fa']
   guards={'native-so':[' move.b #1,0xffffc2fa'],'native-flight':[' move.b #4,0xffffc2fa'],'native-both':[' move.b #5,0xffffc2fa'],'countdown-one':[' move.w #1,0xffffd412'],'countdown-sixty':[' move.w #60,0xffffd412'],'countdown-invalid':[' move.w #-1,0xffffd412'],'ot-so':[' move.w #1,0xfffff090'],'ot-later':[' move.w #2,0xfffff090'],'ot-other':[' move.w #4,0xfffff090'],'ot-invalid':[' move.w #99,0xfffff090']}
   body+=guards.get(z.get('guard'),[]);init,regs=registers(x,y);body+=init+[f' move.w #{0x2000+(z["irq"]<<8)+z["ccr"]},%sr',' jsr 0x1255a',' lea 0xffff4e00,%a0',f' lea 0x{0xffff4000+70*j:x},%a1',' moveq #32,%d0',f'packet{j}: move.w (%a0)+,(%a1)+',f' dbra %d0,packet{j}',' move.l 0xffffbf94,(%a1)']
  body+=capture_ram(0xffff4000+70*len(ss));return run(o/f'commit-{profile}-{batch}','\n'.join(body),70*len(ss)+len(CANARY),[(0x22F000,CANARY),(0x2d4c00,stub),(0x12562,bytes.fromhex('4ef9002d4c00'))],profile)
 def pair(batch):
  old,new=trial(batch,'parent'),trial(batch,'current');ss=specs[batch*16:batch*16+16]
  for j,z in enumerate(ss):
   oldpacket,newpacket=old[j*70:(j+1)*70],new[j*70:(j+1)*70];assert oldpacket[:66]==newpacket[:66],('actual native boundary GPR/SR/SP',z,abi(oldpacket),abi(newpacket));ctx=abi(newpacket);_,regs=registers(*z['point']);assert ctx==dict(regs=regs,sr=abi(oldpacket)['sr'],sp=0xfffff8fc) and (ctx['sr']&0xffef)==0x2004+(z['irq']<<8),('native CLR flags/return and native X',z,ctx);previous=struct.unpack_from('>hh',oldpacket,66);want=oracle(*previous) if z['option']==1 and (not z.get('guard') or z['guard'].startswith('countdown-')) else previous;assert struct.unpack_from('>hh',newpacket,66)==want,('must normalize final native-adjusted point',z,previous,want)
  before,after=bytearray(old[70*len(ss):]),bytearray(new[70*len(ss):]);before[0xbf94-0xb000:0xbf98-0xb000]=after[0xbf94-0xb000:0xbf98-0xb000];assert before==after,('commit changed actor/puck/clock/score/RNG/other RAM',batch);return dict(contexts=ss,whole_active_RAM_and_RNG_after_batch_equal_except_point=True)
 with ThreadPoolExecutor(max_workers=3) as pool:rows=list(pool.map(pair,range((len(specs)+15)//16)))
 print('PASS',len(specs),'actual native stoppage-boundary Off/On/invalid/special-mode full ABI contexts;',len(rows),'whole RAM comparisons',flush=True);return dict(batches=rows)
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 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,0x78,0x79,255]]
 def trial(batch):
  ss=specs[batch*16:batch*16+16];body=[' move.b #1,0xa130f1']
  for j,z in enumerate(ss):
   rec=0x209a95+4*z['slot'];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,0xfffff848'];init,regs=registers(12,-34);body+=init+[' move.w #0x271f,%sr',' jsr 0x224008']+snapshot(0xffff4000+j*68)+[f' move.w 0xfffff848,0x{0xffff4000+j*68+66:x}']
  raw=run(o/f'storage-load-{batch}','\n'.join(body),68*len(ss));rows=[]
  for j,z in enumerate(ss):
   _,regs=registers(12,-34);ctx=abi(raw[j*68:]);value=struct.unpack_from('>H',raw,j*68+66)[0];want=1;assert ctx==dict(regs=regs,sr=0x271f,sp=0xfffff900) and value==want,('record validation/fallback/full ABI',z,value,ctx);rows.append(z|dict(value=value))
  return rows
 with ThreadPoolExecutor(max_workers=3) as pool:loads=sum(pool.map(trial,range((len(specs)+15)//16)),[])
 writes=[]
 for slot in range(4):
  for value in [0,1,99]:
   p=o/f'storage-put-{slot}-{value}';p.mkdir(exist_ok=True);b=bytearray((i*37+19)&255 for i in range(32768));b[0x4d22:0x4d4a]=bytes.fromhex('619e609f619e609f718e708f718e708f738c728d738c728d758a748b758a748b7689768976897689');b[0x4d4a:0x4d52]=bytes.fromhex('7887788778877887');b=bytearray(pack(b)[1::2]);(p/'battery.srm').write_bytes(pack(b));body=[' move.b #1,0xa130f1',f' move.w #{int(slot>0)},0xfffff06c',f' move.w #{slot},0xfffff06a',f' move.w #{value},0xfffff848',' jsr 0x22400c',' move.b #0,0xa130f1',' move.w 0xfffff848,0xffff4000'];run(p,'\n'.join(body),2);after=(p/'battery.srm').read_bytes()[1::2];allowed={0x4d4a+2*slot,0x4d4b+2*slot,0x5ffe,0x5fff};diff=[i for i,(x,y) in enumerate(zip(b,after)) if x!=y];assert set(diff)<=allowed;want=0x79 if value==1 else 0x78;assert after[0x4d4a+2*slot:0x4d4c+2*slot]==bytes([want,want^255]);assert after[0x5fff]==sum(after[0x4000:0x5ffe])%256 and after[0x5ffe]==after[0x5fff]^255;writes.append(dict(slot=slot,value=value,changed=diff))
 print('PASS',len(loads),'storage/full ABI contexts and12complete64KB separated writes',flush=True);return dict(loads=loads,writes=writes)
from faceoff_dots_scenarios import Scenarios
scenario=Scenarios(a,h,o,asm,core,pack,run,registers)
suites={'geometry':geometry_suite,'commit':commit_suite,'storage':storage_suite,'menu':scenario.menu,'observer':scenario.observer,'flow':scenario.flow,'lifetime':scenario.lifetime}
sections={name:function() for name,function in suites.items() if a.section in ['all',name]}
assert set(sections)==(set(suites) if a.section=='all' else {a.section}),('missing painted-dot suite',sections.keys())
(o/'results.json').write_text(json.dumps(dict(rom_sha256=sha(a.rom),baseline_sha256=sha(a.baseline),sections=sections),indent=2)+'\n')
