#!/usr/bin/env python3
"""Native Hard playbook decisions, actual hook sites, guards and register ABI.
The native RNG is forced to an eligible roll only in this decision probe;
paired live gameplay uses the unmodified RNG and complete action execution.
"""
from pathlib import Path
import argparse,json,os,struct,subprocess
ap=argparse.ArgumentParser(description=__doc__);ap.add_argument('rom',type=Path);ap.add_argument('output',type=Path);a=ap.parse_args();h=Path(__file__).resolve().parent;o=a.output.resolve();o.mkdir(parents=True,exist_ok=True)
cases=[]
for team in (0,1):
 for direction in (-1,1):
  for x in (-60,60):
   for kind in ('pressure','point','receive','skate','frame'):
    cases.append(dict(kind=kind,team=team,direction=direction,x=x))
for kind in ('pressure','point','receive','skate','frame'):
 for key,v in [('difficulty',0),('difficulty',2),('flags',8),('flags',32),('unavailable',4),('whistle',1),('view',128),('offside',128),('penalty',4),('special',1),('special',4),('role',0),('role',-1)]:
  cases.append(dict(kind=kind,**{key:v}))
for kind in ('point','receive','frame'):
 for key,v in [('blocked',True),('depth',87),('depth',253),('x',110)]:cases.append(dict(kind=kind,**{key:v}))
for key,v in [('friendly',True),('loose',True)]:cases.append(dict(kind='pressure',**{key:v}))
for depth in (88,144,145):cases.append(dict(kind='point',depth=depth))
for depth in (159,160,252):cases.append(dict(kind='receive',depth=depth))
cases += [dict(kind='skate',friendly=True),dict(kind='skate',wrong_target=True)]
cases += [dict(kind='frame',expired=True),dict(kind='frame',bad_magic=True),dict(kind='frame',missed=True),dict(kind='frame',in_flight=True)]
for t in [0,1]:
 for d in [-1,1]:
  for motion in ['approach','away','stopped']:cases.append(dict(kind='skate',armed=True,motion=motion,team=t,direction=d))
for extra in [dict(near=True,near_depth=16),dict(near=True,near_depth=34),dict(near=True,near_depth=35),dict(depth=112),dict(x=110),dict(blocked=True),dict(expired=True),dict(bad_magic=True),dict(wrong_arm=True),dict(flags=8),dict(difficulty=0),dict(special=4)]:cases.append(dict(kind='skate',armed=True,motion='approach',**extra))
if 'PLAYBOOK_BATCH' not in os.environ:
 rows=[]
 for batch in range((len(cases)+15)//16):
  env=dict(os.environ,PLAYBOOK_BATCH=str(batch));dest=o/str(batch)
  subprocess.run(['python3',str(Path(__file__)),str(a.rom.resolve()),str(dest)],env=env,check=True)
  rows+=json.loads((dest/'results.json').read_text())
 (o/'results.json').write_text(json.dumps(rows,indent=2)+'\n');print('PASS',len(rows),'native playbook decisions/guards/register/RNG cases');raise SystemExit(0)
batch=int(os.environ['PLAYBOOK_BATCH']);cases=cases[16*batch:16*(batch+1)]
with_position=a.rom.read_bytes()[0x225000:0x225002]!=bytes.fromhex('ffff')
body=[' .text',' .globl _start','_start:',' move.w #0x2700,%sr',' movea.l #0xFFF800,%sp']
expected=[]
for n,s in enumerate(cases):
 team=s.get('team',0);direction=s.get('direction',1);actor=team*6+2;opp=(1-team)*6;x=s.get('x',60);depth=s.get('depth',112 if s['kind']=='point' else 210)
 b=0xffb04a+128*actor
 body+=[' clr.l 0xFFF086',' clr.w 0xFFF08A',' lea 0xFFB04A,%a0',' move.w #383,%d0',f'clear{n}: clr.l (%a0)+',f' dbra %d0,clear{n}']
 for i in range(12):body += [f' move.w #-1,0x{0xffb04a+128*i+0x34:X}']
 body += [f' lea 0x{b:X},%a3',f' move.w #{x},(%a3)',f' move.w #{depth*direction},0x14(%a3)',f' move.w #{s.get("role",4)},0x34(%a3)',f' move.w #{actor},0x52(%a3)',f' move.b #{(128 if direction==1 else 0)+(64 if team else 0)+s.get("flags",0)},0x62(%a3)',f' move.b #{s.get("unavailable",0)},0x63(%a3)',f' move.w #{s.get("difficulty",1)},0xFFF07A',f' move.w #{65535 if s.get("loose") else actor if s["kind"]=="point" or s.get("friendly") else opp+2},0xFFB7AA',f' move.w #{x+30 if s["kind"]=="receive" else 90},0xFFB74A',f' move.w #{(depth if s["kind"]=="receive" else -140)*direction},0xFFB75E',' lea 0xFFC6CE,%a2',' clr.b 0x30(%a2)']
 for addr,key in [(0xc2ea,'whistle'),(0xc2ec,'view'),(0xc2ee,'offside'),(0xc2f2,'penalty'),(0xc2fa,'special')]:body.append(f' move.b #{s.get(key,0)},0x{0xff0000+addr:X}')
 if s['kind']=='skate':body += [' move.w #-1,0xFFB7AA',f' move.w #{actor+1 if s.get("wrong_target") else actor},0xFFBEE0']
 if s['kind']=='skate' and s.get('armed'):
  body += [f' move.w #{0 if s.get("bad_magic") else 0x5042},0xFFF086',f' move.w #{actor+2 if s.get("wrong_arm") else actor+1},0xFFF088',f' move.w #{0 if s.get("expired") else 64},0xFFF08A',' clr.w 0xFFB772',f' move.w #{direction*(256 if s.get("motion")=="approach" else -256 if s.get("motion")=="away" else 0)},0xFFB774']
 if s['kind']=='skate' and s.get('near'):
  body += [f' move.w #{(depth-s.get("near_depth",16))*direction},0xFFB75E']
 if s['kind']=='skate' and s.get('friendly'):body += [f' move.w #{actor},0xFFB7AA']
 if s['kind']=='frame':
  body += [f' move.w #{0 if s.get("bad_magic") else 0x5042},0xFFF086',f' move.w #{actor+1},0xFFF088',f' move.w #{0 if s.get("expired") else 64},0xFFF08A',f' move.w #{-1 if s.get("in_flight") else opp if s.get("missed") else actor},0xFFB7AA',f' move.w #{actor},0xFFBEE0']
 if s.get('blocked'):
  ob=0xffb04a+128*opp;body += [f' move.w #1,0x{ob+0x34:X}',f' move.w #{x//2},0x{ob:X}',f' move.w #{(depth+264)//2*direction},0x{ob+0x14:X}']
 body += [' move.l #0x11223344,%d1',' move.l #0x22334455,%d2',' move.l #0x33445566,%d3',' move.l #0x44556677,%d4',' move.l #0x55667788,%d5',' move.l #0x66778899,%d6',' move.l #0x778899AA,%d7',' movea.l #0xFF1234,%a0',' movea.l #0xFF2346,%a1',' movea.l #0xFFCC00,%a6',' move.w #7,%d0',' clr.w 0xFF3FE0',f' jsr 0x{dict(pressure=0xe9d4,point=0x2dd004,receive=0xec0e,skate=0xec60,frame=0x2dd010)[s["kind"]]:X}',f' lea 0x{0xff4000+64*n:X},%a4',' move.w %d0,(%a4)+',' move.w %d1,(%a4)+',' movem.l %d2-%d7/%a0-%a3/%a6,(%a4)',' move.w 0xFF3FE0,44(%a4)',' move.l 0xFFF086,48(%a4)']
 guards=any(s.get(k,0) for k in ('flags','unavailable','whistle','view','offside','penalty','special')) or s.get('role',4)<=0 or s.get('difficulty',1)!=1
 if s['kind']=='pressure':result=[7,0x3344] if guards or s.get('friendly') or s.get('loose') else [90,-140*direction]
 elif s['kind']=='point':result=[int(not guards and 88<=depth<=144 and abs(x)<=100 and not s.get('blocked')),0x3344]
 elif s['kind'] in ('skate','frame'):result=[7,0x3344]
 else:
  # Normal/Easy invoke original PuckOnAttackHalf: private stub returns 9.
  result=[9 if guards else 0,0x3344]
 expected.append(result)
body+=[' move.l #0x504C4159,0xFF3FF0','end: bra.s end'];(o/'driver.s').write_text('\n'.join(body)+'\n');subprocess.run([str(h.parent/'m68kasm.sh'),str(o/'driver.s'),'2D5000',str(o/'code.bin')],check=True,stdout=subprocess.DEVNULL)
b=bytearray(a.rom.read_bytes());c=(o/'code.bin').read_bytes();assert len(c)<0x3000 and b[0x2d5000:0x2d5000+len(c)]==b'\xff'*len(c);b[0x2d5000:0x2d5000+len(c)]=c;struct.pack_into('>I',b,4,0x2d5000)
for pc,code in [(0xc2f2,'33fc0001ffff3fe04e75'),(0x1073a,'4e75'),(0x105a2,'33fc0001ffff3fe0700772094e75'),(0x15464,'4e75'),(0xec68,'4e75'),(0xec14,'4e75'),(0xea04,'4e75'),(0xe9de,'4e75'),(0x11086,'33fc0001ffff3fe070004e75'),(0xf6c44,'70094e75')]:b[pc:pc+len(bytes.fromhex(code))]=bytes.fromhex(code)
(o/'driver.bin').write_bytes(b);(o/'commands.txt').write_text(f'500 dump ff3ff0 4 done\n500 dump ff4000 {64*len(cases)} results\n501 exit\n');env={k:v for k,v in os.environ.items() if not k.startswith('GPGX_')};env['GPGX_DEV']='513'
with (o/'log.txt').open('w') as log:subprocess.run(['python3',str(h/'test_core.py'),str(o/'driver.bin'),str(o/'commands.txt'),str(o)],env=env,stdout=log,stderr=log,check=True,timeout=30)
d={l.split()[1]:bytes.fromhex(' '.join(l.split()[2:])) for l in (o/'commands.out').read_text().splitlines()};assert d['done']==b'PLAY'
rows=[]
for n,(s,e) in enumerate(zip(cases,expected)):
 raw=d['results'][64*n:64*(n+1)];got=list(struct.unpack_from('>2h',raw));assert got==e,(s,got,e)
 regs=list(struct.unpack_from('>11I',raw,4));team=s.get('team',0);assert regs==[0x22334455,0x33445566,0x44556677,0x55667788,0x66778899,0x778899aa,0xff1234,0xff2346,0xffc6ce,0xffb04a+128*(team*6+2),0xffcc00],(s,regs)
 rng=int.from_bytes(raw[48:50],'big')
 guards=any(s.get(k,0) for k in ('flags','unavailable','whistle','view','offside','penalty','special')) or s.get('role',4)<=0 or s.get('difficulty',1)!=1
 action=int(not guards and not any(s.get(k) for k in ('expired','bad_magic','missed','in_flight','blocked')) and 160<=s.get('depth',210)<=252 and abs(s.get('x',60))<=92) if s['kind']=='frame' else e[0] if s['kind']=='point' else int(not guards and not s.get('friendly') and not s.get('wrong_target')) if s['kind']=='skate' else 0
 if s['kind']=='skate' and s.get('armed') and not guards and not s.get('friendly') and not s.get('wrong_target'):
  hold=not(s.get('near') and s.get('near_depth',16)+abs(90-s.get('x',60))<=64) and not any(s.get(k) for k in ['bad_magic','expired','wrong_arm','blocked']) and s.get('motion')=='approach' and 160<=s.get('depth',210)<=252 and abs(s.get('x',60))<=92
  # New positioning retains the native receive path before any native brake.
  action=1 if with_position else int(not hold)
 assert rng==action,(s,rng,action)
 armed=int.from_bytes(raw[52:56],'big')
 if s['kind']=='receive':
  should=not guards and 160<=s.get('depth',210)<=252 and abs(s.get('x',60))<=92 and not s.get('blocked')
  assert armed==((0x5042<<16)|(team*6+3) if should else 0),(s,hex(armed))
 if s['kind']=='frame' and action:assert armed==0,(s,'pending shot did not clear')
 rows.append(dict(spec=s,observed=got,rng_calls=rng,armed=hex(armed)))
(o/'results.json').write_text(json.dumps(rows,indent=2)+'\n');print('PASS',len(rows),'native playbook decisions/guards/register/RNG cases')
