Files
EscapistsReintroduced/tools/prison_approach_evidence.py

134 lines
11 KiB
Python

"""Independent route qualification from raw native records; never sends input."""
import math
from ordinary_gate_evidence import join_gate_snapshots, validate_gate_geometry
from ordinary_look_evidence import validate_look_evidence
def need(value, message):
if not value:
raise ValueError(message)
def number(value):
need(type(value) in (int, float) and math.isfinite(value), 'invalid numeric evidence')
return value
def horizontal(a, b):
return math.hypot(a[0]-b[0], a[2]-b[2])
def norm(v):
return math.sqrt(sum(x*x for x in v))
def yaw(v):
return math.degrees(math.atan2(v[0], v[2]))
def angle(a, b):
return (a-b+180)%360-180
def neutral(s):
i=s['input']
return abs(i['forward']) <= .01 and norm(i['input']) <= .01 and norm(i['velocity']) <= .05
def validate_approach_evidence(content, nonce, look_proof, proof):
result={'verified':False,'errors':[], 'scope':'one private ordinary-input approach to the interior prison gate waypoint; no unlock, escape, visual, multiplayer or FPS qualification', 'held_action_verified':False,'escape_verified':False}
try:
need(type(proof) is dict and proof.get('requested') is True and proof.get('completed') is True and proof.get('nonce')==nonce, 'approach did not complete under this nonce')
need(not proof.get('error') and proof.get('paired_release_errors')==[] and proof.get('unconfirmed_held_key') is None, 'approach input did not end cleanly')
profile=proof.get('profile','standard')
need(type(profile) is str and profile in ('standard','coarse'), 'unknown approach profile')
max_pixels,max_angle,max_pulses=(384,35,16) if profile=='coarse' else (128,15,12)
result['profile']=profile
start=number(proof['started_monotonic']); finish=number(proof['finished_monotonic']); deadline=number(proof['deadline_monotonic'])
need(0 <= start < finish <= deadline and deadline-start <= 35, 'approach helper deadline invalid')
events=proof['events']; stops=proof['stops']; motions=proof['motions']; references=proof['samples']
need(type(events) is list and 2 <= len(events) <= 2*max_pulses and len(events)%2==0, 'expected bounded W key pairs')
need(type(stops) is list and len(stops)*2==len(events), 'each W pair needs a native full-stop interval')
need(type(motions) is list and len(motions)<=16 and type(references) is list and 2 <= len(references) <= 450, 'route reference bounds invalid')
ids=[r['sample'] for r in references]
need(all(type(i) is int for i in ids) and ids==sorted(set(ids)), 'route sample references repeated or reversed')
data=join_gate_snapshots(content,nonce); samples=data['snapshots']; first,last=ids[0],ids[-1]
need(type(proof.get('final_sample')) is int and proof['final_sample']==last, 'final native reference differs from last sample')
need(first >= look_proof['return_sample'] and first in samples and last in samples, 'route is not after native look calibration')
receipts={}; previous_receipt=start-.5
for reference in references:
i=reference['sample']; need(i in samples and type(reference['real_ms']) is int and reference['real_ms']==samples[i]['real_ms'], 'helper sample clock differs from native source')
received=number(reference['received_monotonic'])
need(previous_receipt<=received<=finish, 'native receipt timestamps reversed or outside route')
receipts[i]=received; previous_receipt=received
geometry=validate_gate_geometry(content,nonce,first,last)
need(geometry['verified'], 'native route geometry rejected: '+str(geometry['errors']))
look=validate_look_evidence(content,nonce,look_proof,events[:2])
need(look['verified'], 'native calibration rejected: '+str(look['errors']))
need(start >= look_proof['finished_monotonic'], 'route started before calibration finished')
origin=samples[first]['geometry']; center=origin['prison']; hinge=origin['gate']; handle=origin['context']
dx,dz=center[0]-hinge[0],center[2]-hinge[2]; separation=math.hypot(dx,dz)
need(5.75 <= separation <= 6.25 and min(abs(dx),abs(dz)) <= .1, 'unsupported native prison geometry')
inward=(dx/separation,dz/separation)
need(abs(origin['gate_forward'][0]*inward[0]+origin['gate_forward'][2]*inward[1])>=.99 and horizontal(handle,hinge)<=4.5,'native gate orientation or handle invalid')
target=(handle[0]+1.5*inward[0],center[1],handle[2]+1.5*inward[1])
def inside(v): return abs(v[0]-center[0])<=5.25 and abs(v[2]-center[2])<=5.25 and abs(v[1]-center[1])<=3
need(inside(target), 'interior waypoint outside authored bounds')
ready=data['ready']; need(samples[last]['real_ms']-ready['real_ms']<35000,'native approach exceeded original READY bound')
path=0.; previous=None
for i in range(first,last+1):
need(i in samples,'missing raw route sample')
s=samples[i]; p=s['input']['origin']; g=s['geometry']; a=s['action']
need(inside(p) and norm(s['input']['velocity'])<=5,'body left interior motion envelope')
move=s['input']['input'];need(abs(move[0])<=.01 and abs(move[1])<=.01 and -.01<=move[2]<=1.01 and -.01<=s['input']['forward']<=1.01, 'unexpected native movement input')
need(g['door_state']==0 and g['door_control']==0 and a['pressed']==0 and not a['performing'] and a['progress']==0,'gate action or door was operated during approach')
if previous:
dt=s['real_ms']-previous['real_ms']; step=horizontal(p,previous['input']['origin'])
need(0<dt<=500 and step<=.25+5*dt/1000,'route contains a clock gap or discontinuous displacement')
path+=step
previous=s
need(path<=6 and .75<horizontal(samples[first]['input']['origin'],target)<=6,'native route path or initial distance outside scope')
need(horizontal(samples[last]['input']['origin'],target)<=.75 and neutral(samples[last]),'body did not stop at interior waypoint')
def stable_end(index):
need(index in samples and neutral(samples[index]),'native stop is not neutral')
end=samples[index]; cursor=index-1
while cursor>=first and end['real_ms']-samples[cursor]['real_ms']<100:
need(neutral(samples[cursor]),'native stop was not sustained');cursor-=1
need(cursor>=first and neutral(samples[cursor]) and horizontal(end['input']['origin'],samples[cursor]['input']['origin'])<=.02,'native stop lacks two stable samples across100ms')
need(abs(angle(yaw(end['look']['direction']),yaw(samples[cursor]['look']['direction'])))<=.2, 'native camera not settled at stop')
stable_end(last)
last_time=start; previous_stop=first; largest=None
for n,stop in enumerate(stops):
down,up=events[2*n:2*n+2]
need(down.get('key')=='W' and down.get('down') is True and up.get('key')=='W' and up.get('down') is False and not down.get('finally') and not up.get('finally'),'unexpected route key event')
td,tu=number(down['monotonic']),number(up['monotonic']);need(last_time<=td<tu<=finish and tu-td<=.4,'W hold/order outside route bound');last_time=tu
before,after=stop['before_sample'],stop['after_sample']
need(type(before) is int and type(after) is int and previous_stop<=before<after<=last,'invalid pulse native references');need(before in receipts and after in receipts and receipts[before]<=td<tu<=receipts[after] and td-receipts[before]<=.5, 'W timestamps contradict native receipt references');stable_end(before);stable_end(after)
begin,end=samples[before],samples[after]; a,b=begin['input']['origin'],end['input']['origin'];distance=horizontal(a,b)
need(.05<=distance<=1.25 and horizontal(a,target)-horizontal(b,target)>=.05,'native pulse missed or moved away')
need(horizontal(a,target)>(largest if largest is not None else 1.25)+.25,'native pulse lacked stopping margin')
wanted=math.degrees(math.atan2(target[0]-a[0],target[2]-a[2]));need(abs(angle(wanted,yaw(begin['look']['direction'])))<=3 and abs(angle(yaw(end['look']['direction']),yaw(begin['look']['direction'])))<=5,'pulse heading invalid')
need(any(samples[i]['input']['forward']>.01 and samples[i]['input']['input'][2]>.01 for i in range(before+1,after+1)),'W pulse has no observed forward input')
largest=max(largest or 0,distance);previous_stop=after
pixels=0;previous_motion=first;previous_motion_time=start;gain=look['response_yaw_degrees']/24
need(.01<=abs(gain)<=.5, 'native calibration gain outside approach range')
for motion in motions:
before,after=motion['after_sample'],motion['response_sample']; dx=motion['dx'];at=number(motion['monotonic'])
need(type(dx) is int and 1<=abs(dx)<=max_pixels and type(motion['dy']) is int and motion['dy']==0,'relative step exceeds route bounds')
need(type(before) is int and type(after) is int and previous_motion<=before<after<=last and start<=at<=finish,'relative motion references/order invalid')
need(previous_motion_time<=at and before in receipts and after in receipts and receipts[before]<=at<=receipts[after] and at-receipts[before]<=.5, 'relative motion timestamps contradict native receipts')
previous_motion_time=at
a,b=samples[before],samples[after];change=angle(yaw(b['look']['direction']),yaw(a['look']['direction']))
need(.05<=abs(change)<=max_angle and change*dx*gain>0,'native turn did not acknowledge proposal')
for i in range(before,after+1):
need(neutral(samples[i]) and horizontal(samples[i]['input']['origin'],a['input']['origin'])<=.05,'body moved during heading correction')
turn=angle(yaw(samples[i]['look']['direction']),yaw(a['look']['direction']))
need(abs(turn)<=max_angle and (abs(turn)<.05 or turn*dx*gain>0), 'intermediate native turn exceeded bounds or reversed')
stable_end(after);pixels+=abs(dx);previous_motion=after
need(all(not (events[i]['monotonic'] <= m['monotonic'] <= events[i+1]['monotonic']) for m in motions for i in range(0,len(events),2)), 'mouse motion overlaps W hold')
need(pixels<=2048,'relative pixel sum exceeds route bound')
result.update(verified=True,first_sample=first,last_sample=last,native_duration_ms=samples[last]['real_ms']-samples[first]['real_ms'],path_metres=path,target=list(target),final_distance_metres=horizontal(samples[last]['input']['origin'],target),pulse_count=len(stops),turn_count=len(motions),geometry=geometry)
except (ValueError,KeyError,TypeError,OverflowError,IndexError) as error:
result['errors'].append(str(error))
return result