120 lines
5.6 KiB
Python
120 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Read-only trapdoor panel/contact checks for actual or overlaid block readers.
|
|
|
|
Pass a ``state_reader(x, y, z)`` returning canonical block data. Each fixture is
|
|
``{'pos': [x,y,z], 'support': [x,y,z]}`` for an adjacent support cell, or uses
|
|
``'edge': 'south'`` for an intended edge within the trapdoor's own cell.
|
|
Support materials and partial support shapes must be validated by the caller;
|
|
this check measures contact with that cell's face and rejects an empty cell.
|
|
|
|
Shape source: pinned Paper 26.2 TrapDoorBlock.getShape / static initializer,
|
|
Block.boxZ(16,13,16), Shapes.rotateAll. An open NORTH trapdoor occupies the
|
|
SOUTH 3/16 strip. HALF affects closed panels only. No world/file/network writes.
|
|
|
|
>>> world = {(0, 0, 0): 'minecraft:spruce_trapdoor[facing=east,half=bottom,open=true]', (1, 0, 0): 'minecraft:spruce_planks'}
|
|
>>> read = lambda x, y, z: world.get((x,y,z), 'minecraft:air')
|
|
>>> check_contact(read, (0,0,0), support=(1,0,0))['gap_blocks']
|
|
0.8125
|
|
>>> world[(0,0,0)] = 'minecraft:spruce_trapdoor[open=true,facing=west,half=top]'
|
|
>>> check_contact(read, (0,0,0), support=(1,0,0))['passed']
|
|
True
|
|
>>> check_contact(read, (0,0,0), edge='north')['passed']
|
|
False
|
|
>>> world[(0,0,0)] = 'minecraft:iron_trapdoor[open=false,facing=south,half=top]'
|
|
>>> check_contact(read, (0,0,0), edge='up')['passed']
|
|
True
|
|
"""
|
|
|
|
NORMALS = {'west': (-1, 0, 0), 'east': (1, 0, 0),
|
|
'down': (0, -1, 0), 'up': (0, 1, 0),
|
|
'north': (0, 0, -1), 'south': (0, 0, 1)}
|
|
THICKNESS = 3 / 16
|
|
AIR = {'minecraft:air', 'minecraft:cave_air', 'minecraft:void_air'}
|
|
|
|
|
|
def panel_bounds(state):
|
|
"""Return local AABB minimum/maximum and its thickness axis (X=0,Y=1,Z=2)."""
|
|
name, separator, raw = state.partition('[')
|
|
if not name.startswith('minecraft:') or not name.endswith('_trapdoor') or not separator or not raw.endswith(']'):
|
|
raise ValueError('Expected explicit trapdoor block data')
|
|
values = dict(part.split('=', 1) for part in raw[:-1].split(','))
|
|
if values.get('open') == 'true':
|
|
# Bounds are in block units; the panel sits opposite its facing.
|
|
strips = {'north': (2, 1 - THICKNESS, 1), 'south': (2, 0, THICKNESS),
|
|
'east': (0, 0, THICKNESS), 'west': (0, 1 - THICKNESS, 1)}
|
|
if values.get('facing') not in strips:
|
|
raise ValueError('Open trapdoor needs a horizontal facing')
|
|
axis, start, end = strips[values['facing']]
|
|
elif values.get('open') == 'false' and values.get('half') in {'top', 'bottom'}:
|
|
axis = 1
|
|
start, end = (1 - THICKNESS, 1) if values['half'] == 'top' else (0, THICKNESS)
|
|
else:
|
|
raise ValueError('Trapdoor needs explicit open and valid half properties')
|
|
minimum, maximum = [0., 0., 0.], [1., 1., 1.]
|
|
minimum[axis], maximum[axis] = start, end
|
|
return minimum, maximum, axis
|
|
|
|
|
|
def _position(pos):
|
|
result = tuple(pos)
|
|
if len(result) != 3 or any(type(value) is not int for value in result):
|
|
raise ValueError('Position must contain three integer block coordinates')
|
|
return result
|
|
|
|
|
|
def check_contact(state_reader, pos, *, support=None, edge=None):
|
|
"""Measure a panel's gap/alignment to a requested edge or adjacent cell face.
|
|
|
|
``passed`` requires the broad panel face to be parallel and flush. Merely
|
|
touching with a thin side edge does not pass. A support fixture also requires
|
|
a nonair observed support; this is not a full support collision-shape audit.
|
|
"""
|
|
pos = _position(pos)
|
|
if (support is None) == (edge is None):
|
|
raise ValueError('Specify exactly one adjacent support or intended edge')
|
|
support_state = None
|
|
if support is not None:
|
|
support = _position(support)
|
|
delta = tuple(target - origin for origin, target in zip(pos, support))
|
|
edge = next((name for name, normal in NORMALS.items() if normal == delta), None)
|
|
if edge is None:
|
|
raise ValueError('Support must share one face with the trapdoor cell')
|
|
support_state = state_reader(*support)
|
|
if edge not in NORMALS:
|
|
raise ValueError('Unknown intended edge')
|
|
state = state_reader(*pos)
|
|
minimum, maximum, thickness_axis = panel_bounds(state)
|
|
normal = NORMALS[edge]
|
|
axis = next(i for i, value in enumerate(normal) if value)
|
|
gap = minimum[axis] if normal[axis] < 0 else 1 - maximum[axis]
|
|
parallel = axis == thickness_axis
|
|
support_present = support_state is None or support_state.partition('[')[0] not in AIR
|
|
result = {'pos': list(pos), 'state': state, 'edge': edge,
|
|
'panel_bounds': {'min': minimum, 'max': maximum},
|
|
'panel_parallel_to_face': parallel, 'gap_blocks': gap,
|
|
'passed': parallel and gap == 0 and support_present}
|
|
if support is not None:
|
|
result.update(support=list(support), support_state=support_state,
|
|
support_present=support_present)
|
|
return result
|
|
|
|
|
|
def audit_contacts(state_reader, fixtures):
|
|
"""Return a compact summary and per-fixture geometric evidence."""
|
|
checks = []
|
|
for fixture in fixtures:
|
|
result = check_contact(state_reader, fixture['pos'], support=fixture.get('support'), edge=fixture.get('edge'))
|
|
if 'name' in fixture:
|
|
result['name'] = fixture['name']
|
|
checks.append(result)
|
|
failures = [check for check in checks if not check['passed']]
|
|
return {'passed': not failures, 'checked': len(checks), 'failed': len(failures),
|
|
'max_gap_blocks': max((check['gap_blocks'] for check in checks), default=0),
|
|
'checks': checks}
|
|
|
|
|
|
if __name__ == '__main__':
|
|
import doctest
|
|
result = doctest.testmod()
|
|
raise SystemExit(bool(result.failed))
|