Release 2026-11-04-r2 · SHA256 9fd898f725fd7dfdddc23a5d999ad82aa2dfb6d7e0e2845d0b23ee6fd47b89ba · Click a line number for a direct link.
1#!/usr/bin/env python32"""Optional painted-dot policy: native geometry, exact boundary ABI and saves.3Declared CPU/input fixtures and image controls are not human or win-rate trials.4"""5from pathlib import Path6from concurrent.futures import ThreadPoolExecutor7import argparse,hashlib,json,os,struct,subprocess8ap=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'910def asm(p,body,origin=0x2d5000):11 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()12def core(p,rom,commands,settings=None):13 (p/'commands.txt').write_text('\n'.join(commands)+'\n')14 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)15def run(p,body,size,patches=(),profile='current'):16 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)17 for patch in patches:18 at,data=patch[:2]19 expected=patch[2] if len(patch)==3 else bytes.fromhex('08f80002c2ea') if at==0x12562 else b'\xff'*len(data)20 assert b[at:at+len(data)]==expected,(hex(at),'instrumentation reservation');b[at:at+len(data)]=data21 (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)22 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']23def oracle(x,y):24 if not(-70<=x<=70 and -206<=y<=206):return x,y25 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)]26 return min(points,key=lambda p:(p[0]-x)**2+(p[1]-y)**2)27def 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}']28def 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'))29def registers(x,y):30 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)31 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:])],regs32CANARY=bytes((i*37+(i>>8)*11+19)&255 for i in range(0x4850))33def copy_native():return [' lea 0x22F000,%a0',' lea 0xffffb000,%a1',' move.w #0x4850/4-1,%d0','copy: move.l (%a0)+,(%a1)+',' dbra %d0,copy']34def 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']35def geometry_suite():36 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)]37 def exhaustive(item):38 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)])39 for j,p in enumerate(ss):40 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))41 return dict(cases=len(ss),input_sha256=hashlib.sha256(data).hexdigest(),output_sha256=hashlib.sha256(raw).hexdigest())42 with ThreadPoolExecutor(max_workers=3) as pool:grids=list(pool.map(exhaustive,enumerate(batches)))43 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)]44 def contexts(batch):45 ss=specs[batch*16:batch*16+16];body=copy_native()46 for j,z in enumerate(ss):47 init,regs=registers(*z['point']);body+=init+[f' move.w #{0x2000+(z["irq"]<<8)+z["ccr"]},%sr',' jsr 0x224004']+snapshot(0xffff4000+66*j)48 body+=capture_ram(0xffff4000+66*len(ss));raw=run(o/f'geometry-ABI-{batch}','\n'.join(body),66*len(ss)+len(CANARY),[(0x22F000,CANARY)])49 for j,z in enumerate(ss):50 _,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)51 assert raw[66*len(ss):]==CANARY,('geometry touched game RAM/RNG',batch);return dict(contexts=ss,complete_active_RAM_after_batch_exact=True)52 with ThreadPoolExecutor(max_workers=3) as pool:contracts=list(pool.map(contexts,range(16)))53 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')54def commit_suite():55 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)]56 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'])]57 stub=bytes.fromhex('48f97fffffff4e0040f9ffff4e3c23cfffff4e3e4e75')58 def trial(batch,profile):59 ss=specs[batch*16:batch*16+16];body=copy_native()60 for j,z in enumerate(ss):61 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']62 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']}63 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)']64 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)65 def pair(batch):66 old,new=trial(batch,'parent'),trial(batch,'current');ss=specs[batch*16:batch*16+16]67 for j,z in enumerate(ss):68 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)69 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)70 with ThreadPoolExecutor(max_workers=3) as pool:rows=list(pool.map(pair,range((len(specs)+15)//16)))71 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)72def pack(b):73 for off in range(0,len(b),8192):b[off+0x1fff]=sum(b[off:off+0x1ffe])&255;b[off+0x1ffe]=b[off+0x1fff]^25574 raw=bytearray(2*len(b));raw[1::2]=b;return raw75def storage_suite():76 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]]77 def trial(batch):78 ss=specs[batch*16:batch*16+16];body=[' move.b #1,0xa130f1']79 for j,z in enumerate(ss):80 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}']81 raw=run(o/f'storage-load-{batch}','\n'.join(body),68*len(ss));rows=[]82 for j,z in enumerate(ss):83 _,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))84 return rows85 with ThreadPoolExecutor(max_workers=3) as pool:loads=sum(pool.map(trial,range((len(specs)+15)//16)),[])86 writes=[]87 for slot in range(4):88 for value in [0,1,99]:89 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))90 print('PASS',len(loads),'storage/full ABI contexts and12complete64KB separated writes',flush=True);return dict(loads=loads,writes=writes)91from faceoff_dots_scenarios import Scenarios92scenario=Scenarios(a,h,o,asm,core,pack,run,registers)93suites={'geometry':geometry_suite,'commit':commit_suite,'storage':storage_suite,'menu':scenario.menu,'observer':scenario.observer,'flow':scenario.flow,'lifetime':scenario.lifetime}94sections={name:function() for name,function in suites.items() if a.section in ['all',name]}95assert set(sections)==(set(suites) if a.section=='all' else {a.section}),('missing painted-dot suite',sections.keys())96(o/'results.json').write_text(json.dumps(dict(rom_sha256=sha(a.rom),baseline_sha256=sha(a.baseline),sections=sections),indent=2)+'\n')97