"""Settle a defensive pair, then observe native skating with no position resets."""
from pathlib import Path
import json,math
p=Path(outdir);s=json.loads((p/'spec.json').read_text());rows=[];team=s.get('team',0);orient=s.get('orient',1);sign=s.get('sign',1);own=team*6;opp=(1-team)*6;carrier=opp+2;receiver=opp;backs=[own+3,own+4]
def rw(a):return(wram[ram(a)]<<8)|wram[ram(a+1)]
def sw(a):v=rw(a);return v-65536 if v&32768 else v
def ww(a,v):v&=65535;wram[ram(a)]=v>>8;wram[ram(a+1)]=v&255
def obj(i):return 0xb04a+128*i
def put(i,x,y):
 b=obj(i)
 for off,v in [(0,x),(2,0),(0x14,y*orient),(0x16,0),(0x18,0),(0x1a,0),(0x28,0),(0x2a,0),(0x2c,0),(0x2e,-1),(0x30,0),(0x32,0),(0x36,0),(0x40,0),(0x42,8),(0x44,60),(0x46,-220*orient),(0x48,16),(0x58,0),(0x5a,0),(0x5c,0),(0x5e,0)]:ww(b+off,v)
 for off in [0x60,0x61]:wram[ram(b+off)]=255
 wram[ram(b+0x62)]=(64 if i//6 else 0)+(128 if (orient if i//6==team else -orient)==1 else 0);wram[ram(b+0x63)]=0;wram[ram(b+0x64)]=0
 role=[5,3,4,2,1,0][i%6];ww(b+0x34,role);wram[ram(b+0x38)]={5:3,3:3,4:5,2:2,1:2,0:14}[role]

def test_tick(f):
 if f<100:return
 g=obj(own+5);c=obj(carrier)
 if f<=108:
  held.clear();ww(0xd046,1);ww(0xd04a,2);ww(0xf07c,0);ww(0xf0d4,0)
  for i in range(12):put(i,140 if i%2 else -140,0)
  put(own+5,0,246);put(opp+5,0,-246)
  put(carrier,sign*s.get('start_x',104),s.get('depth',220))
  for i in range(4):ww(0xc328+2*i,0);ww(0xc320+2*i,65535)
  ww(0xc328,carrier//6+1);ww(0xc320,carrier);wram[ram(c+0x62)]|=8
  # Keep distant non-goalie actors idle, under explicit human flags only.
  for i in range(12):
   if i%6!=5:wram[ram(obj(i)+0x62)]|=8
  # A positive normalized goal is defended: team-0 attack-direction bit clear.
  for i in range(12):
   wram[ram(obj(i)+0x62)] &= ~128
   if (i//6==team)==(orient<0):wram[ram(obj(i)+0x62)] |=128
  wram[ram(0xc2ea)] &= ~0x53
  if (orient if team==0 else -orient)>0:wram[ram(0xc2ea)] |=2
  for a,mask in [(0xc2ec,128),(0xc2ee,128),(0xc2f2,4),(0xc2fa,5)]:wram[ram(a)] &= ~mask
  ww(0xf07a,s.get('difficulty',1));ww(0xd050,0);ww(0xc468,290);ww(0xd058,1);ww(0xd052,0)
  ww(0xb7aa,carrier);ww(0xb74a,sign*s.get('start_x',104));ww(0xb75e,s.get('depth',220)*orient)
  for a in [0xb74c,0xb760,0xb762,0xb764,0xb772,0xb774,0xb776]:ww(a,0)
  ww(0xbee0,-1);ww(0xbeda,-1);ww(0xf060,0);ww(0xf002,0)
  # Rebuild the native sorted Y broadphase after moving fixture actors.
  # Stationary keepers otherwise retain their captured old cache coordinates,
  # causing false contacts across opposite ends of the rink.
  if f==108:
   for i in range(16):
    ww(obj(i)+0x1c,rw(obj(i)));ww(obj(i)+0x20,rw(obj(i)+0x14))
   order=sorted(range(16),key=lambda i:sw(obj(i)+0x14))
   for rank,i in enumerate(order):
    ww(0xb84a+2*i,rw(obj(i)+0x14));ww(0xb86a+2*i,rank);wram[ram(0xb88a+rank)]=2*i
 else:
  n=f-109
  held.clear()
  if n<s.get('skate_frames',72):
   held[(0,IDS['Left' if sign>0 else 'Right'])]=1
   if n<s.get('diagonal_frames',0):held[(0,IDS['Down' if orient>0 else 'Up'])]=1
  if s.get('shot_at',0)<=n<s.get('shot_at',0)+s.get('charge',8) and s.get('shot_at',0):held[(0,IDS['C'])]=1
  row=dict(frame=n,owner=sw(0xb7aa),carrier=carrier,carrier_xy=[sw(c),sw(c+0x14)*orient],puck=[sw(0xb74a),sw(0xb75e)*orient,sw(0xb762)],keeper=[sw(g),sw(g+0x14)*orient],velocity=[sw(g+0x28),sw(g+0x2a)],anim=rw(g+0x58),anim_frame=rw(g+0x5a),anim_delay=sw(g+0x5c),flags=rw(g+0x62),direction=rw(g+0x54),clock=rw(0xc468),whistle=rw(0xc2ea),score=[rw(0xc6da),rw(0xca3e)],shotplayer=sw(0xbed8))
  rows.append(row)
  if n in (20,40,60,80,100):
   nbytes=core.retro_serialize_size();buf=C.create_string_buffer(nbytes);assert core.retro_serialize(buf,nbytes);(p/f'frame-{n}.st').write_bytes(buf.raw)
 if f==s.get('end_frame',508):(p/'trajectory.json').write_text(json.dumps(rows,indent=2)+'\n')
