Files
EscapistsReintroduced/tools/prison_action_selection_proof.py

152 lines
9.5 KiB
Python

"""One explicit logical-button5 selection trial; adapters own all native I/O.
Each snapshot joins native pose/input/gate and choices at the same nonce/index/
real clock. choices contains nonce,sample,real_ms,selected_id,actions; actions
contains exactly [{class,id,permitted}, ...]. This deliberately supports only
closed disabled ERR_PrisonDoorAction followed by permitted ERR_UnlockPrisonAction.
Their distinct stock/authored names cannot merge in stock action aggregation.
wheel.pulse(5,guard,deadline=...) must return button/down_monotonic/up_monotonic/
released, after actual paired release confirmation. A failure must retain its
own uncertain-button evidence and attempt safe release; this policy cannot send
another button event. No default workflow activates this module.
"""
import copy
import math
from prison_approach_proof import number, vector, length, yaw, delta, horizontal, target_for
class Refused(ValueError):
pass
def need(ok, message):
if not ok:
raise Refused(message)
def choices_for(s, geometry, *, selected_lock=False):
"""Require the exact two-entry experiment, not arbitrary raw-list navigation."""
c = s.get('choices')
need(type(c) is dict and set(c) == {'nonce','sample','real_ms','selected_id','selected_class','actions'}, 'Missing exact native choices')
need(c['nonce'] == geometry['nonce'] and type(c['sample']) is int and c['sample'] == s['sample'] and
number(c['real_ms']) == s['real_ms'], 'Choices are not joined to the native sample')
actions = c['actions']
need(type(actions) is list and len(actions) == 2, 'Selection requires exactly two native actions')
for a, name, permitted in zip(actions, ('ERR_PrisonDoorAction','ERR_UnlockPrisonAction'), (False, True)):
need(type(a) is dict and set(a) == {'class','id','permitted'} and a['class'] == name and
type(a['id']) is int and a['id'] >= 0 and a['permitted'] is permitted,
'Native action class/order/permission differs from bounded trial')
need(actions[0]['id'] != actions[1]['id'], 'Native action IDs collide')
need(type(c['selected_id']) is int and c['selected_id'] == actions[int(selected_lock)]['id'],
'Unexpected native selected action ID')
need(c['selected_class'] == actions[int(selected_lock)]['class'], 'Native selected action class disagrees')
need(s['context_selected'] is True and s['cached_performable'] is True and
s['selected'] is selected_lock and s['progress_bound'] is selected_lock,
'Native selected/context/permission binding disagrees')
return copy.deepcopy(actions)
def perform_selection(guard, require_idle, wheel, observe, geometry, calibration, facing_evidence, report, *, clock, sleep, notify):
need('action_selection_proof' not in report, 'Selection attempt already exists')
pinned = copy.deepcopy(geometry); target_for(pinned)
facing = report.get('facing_proof', {})
need(facing_evidence.get('verified') is True and facing.get('completed') is True and
facing.get('nonce') == pinned['nonce'] and facing.get('geometry') == pinned and
facing_evidence.get('last_sample') == facing.get('final_sample'), 'Independent same-run facing proof required')
prior = facing.get('samples', [])
need(prior and prior[-1]['sample'] == facing['final_sample'], 'Facing final native sample unavailable')
anchor = prior[-1]; body = vector(anchor['origin']); actions = choices_for(anchor, pinned)
started = clock(); deadline = number(facing['deadline_monotonic'])
queued = number(calibration['observer_start_real_ms']); ready = number(calibration['ready_real_ms'])
ready_at = number(calibration['ready_monotonic'])
need(calibration.get('nonce') == pinned['nonce'] and 0 <= queued and 2000 <= ready-queued < 45000 and
0 <= ready_at <= started, 'Original observation clock binding invalid')
original = ready_at+(45000-(ready-queued))/1000-.5
need(deadline <= number(facing['started_monotonic'])+8 and deadline <= original,
'Facing deadline renewed the original lease')
state = {'requested':True,'completed':False,'nonce':pinned['nonce'],'geometry':pinned,
'started_monotonic':started,'finished_monotonic':None,'deadline_monotonic':deadline,
'initial_sample':None,'final_sample':None,'selection_samples':[], 'actions':actions,
'wheel':None,'samples':[]}
report['action_selection_proof'] = state
previous = None; dispatched = False
def consume(raw):
nonlocal previous
s = copy.deepcopy(raw)
index = s['sample']; real = number(s['real_ms']); receipt = number(s['received_monotonic'])
need(type(index) is int and facing['final_sample'] <= index <= 450 and 0 <= receipt <= clock() and
queued <= real < queued+45000, 'Native selection index/clock invalid')
need(s['geometry'] == pinned and s['phase'] == 1 and s['inventory'] is False and
number(s['door_state']) == 0 and number(s['door_control']) == 0, 'Original closed prison context changed')
for k in ('origin','camera','direction','body_forward','input','velocity'): s[k] = vector(s[k])
need(math.dist(body,s['origin']) <= .05 and abs(number(s['forward'])) <= .01 and
length(s['input']) <= .01 and length(s['velocity']) <= .05, 'Body or movement changed during selection')
for k in ('selected','context_selected','cached_performable','progress_bound','performing'):
need(type(s[k]) is bool, 'Malformed native action flag')
need(number(s['pressed']) == 0 and number(s['progress']) == 0 and not s['performing'] and
number(s['duration']) == 4, 'Action began during selection-only trial')
need(.25 <= horizontal(s['camera'],pinned['context']) <= 5, 'Camera/handle bearing invalid')
bearing = math.degrees(math.atan2(pinned['context'][0]-s['camera'][0],pinned['context'][2]-s['camera'][2]))
need(abs(delta(bearing,yaw(s['direction']))) <= 3 and
abs(delta(yaw(s['direction']),yaw(anchor['direction']))) <= .2, 'Facing changed during wheel-only trial')
observed = choices_for(s,pinned,selected_lock=s['selected'])
need(observed == actions, 'Original action identities/order changed')
if not dispatched: need(not s['selected'], 'Lock selected before wheel dispatch')
if previous:
need(not previous['selected'] or s['selected'], 'Native lock selection dropped after wheel acknowledgement')
need(index >= previous['sample'] and real >= previous['real_ms'], 'Native samples reversed')
if index == previous['sample']: need(s == previous, 'Repeated native sample changed')
else: need(index == previous['sample']+1 and 0 < real-previous['real_ms'] <= 500, 'Native sample gap')
if previous is None or index != previous['sample']: state['samples'].append(copy.deepcopy(s))
previous = s
if state['initial_sample'] is None: state['initial_sample'] = index
state['final_sample'] = index
return s
def read():
need(clock() < deadline, 'Original facing/selection deadline expired')
guard.check(); require_idle()
batch = observe()
need(type(batch) is list and 1 <= len(batch) <= 450, 'Missing joined selection samples')
for raw in batch: s = consume(raw)
need(0 <= clock()-s['received_monotonic'] <= .5 and clock() < deadline, 'Native selection evidence stale or expired')
return s
try:
notify({'kind':'action-selection-pending','deadline_monotonic':deadline})
current = read()
need(clock()+1.5 <= deadline, 'Insufficient unchanged lease for one wheel trial')
# No native calls here: adapter rechecks ownership/idle/deadline immediately
# before each request and confirms release on the same connection.
before = current['sample']; request_at = clock()
state['wheel'] = {'button':5,'attempted_monotonic':request_at,'after_sample':before,'confirmed':False}
result = wheel.pulse(5,guard,deadline=min(deadline,request_at+1.5))
need(type(result) is dict and set(result) == {'button','down_monotonic','up_monotonic','released'} and
type(result['button']) is int and result['button'] == 5 and result['released'] is True,
'Wheel paired release not confirmed')
down = number(result['down_monotonic']); up = number(result['up_monotonic'])
need(request_at <= down < up <= clock() < deadline and up-request_at <= 1.5,
'Wheel attempt exceeded immutable lease')
state['wheel'].update(result, confirmed=True)
dispatched = True
stop = min(deadline,up+1.5)
while clock() < stop:
current = read()
need(clock() < stop, 'Native selection acknowledgement exceeded bound')
samples = state['samples']
if len(samples) >= 2:
a,b = samples[-2:]
if a['sample'] > before and a['received_monotonic'] >= down and a['selected'] and b['selected'] and b['sample'] == a['sample']+1 and b['real_ms']-a['real_ms'] >= 100 and abs(delta(yaw(b['direction']),yaw(a['direction']))) <= .2:
state['selection_samples'] = [a['sample'],b['sample']]
state['completed'] = True
return state
sleep(.025)
raise Refused('Single wheel did not select original lock before deadline')
except Exception as error:
state['error'] = str(error)
raise
finally:
state['finished_monotonic'] = clock()
notify({'kind':'action-selection-finished','successful':state['completed']})