Harnesses: retry rejected vanilla runs, observe boat dips at once, fixed piglin barter check

- one_player: vanilla's run is judged by the harness's check before the
  comparison, and run again (up to 3 times) when it is no proof, so a
  vanilla bobber that missed its armor stand on a loaded runner no
  longer reads as a Horizon diff (fishing_hook_entities). Horizon's run
  is never retried.
- boat_submerged: the short dip and the re-submersion are observed at
  the end of their own 20-tick step, not a step later; at ~40 ticks plus
  the client's syncs a loaded host pushed them past the 60 that eject.
- command_exec piglin pickups: what an adult's barter throws comes from
  vanilla's unseeded level random and the piglin picks some of it back
  up; check that none of the gold lies on the ground instead of the
  item count.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Ludovic MarieandClaude Opus 5.5 committed 2026-11-03 03:03:22 +01:00
1 parent 7e5a520e3d
commit 2518376ee0
3 files changed
+28 -10

No files matched your search

+5 -1
View File
@@ -1670,7 +1670,11 @@ def piglin_pickups():
{'op': 'tick', 'n': 140},
cmd('/execute if items entity @e[tag=barter,limit=1] weapon.offhand *'),
cmd('/execute if items entity @e[tag=barter,limit=1] weapon.mainhand minecraft:gold_ingot'),
cmd('/execute if entity @e[type=minecraft:item]'),
# A baby drops nothing. What an adult's barter throws comes from the level's random (vanilla's is
# unseeded), and the piglin picks some of it back up, so only the gold's fate is fixed: none of it lies
# on the ground.
cmd('/execute if entity @e[type=minecraft:item]' if baby
else '/execute if items entity @e[type=minecraft:item] contents minecraft:gold_ingot'),
]
if baby:
ops.append(cmd('/execute if data entity @e[tag=barter,limit=1] Inventory[{id:"minecraft:golden_sword"}]'))
+8 -6
View File
@@ -16,10 +16,11 @@ EXPECTED = []
PROBES = []
def probe(label, mounted, player='Alice'):
def probe(label, mounted, player='Alice', actions=()):
"""A step running `actions`, at whose end `player` must be (or no longer be) a passenger."""
PROBES.append((label, player))
EXPECTED.append(f'{label} mounted={str(mounted).lower()}')
SCRIPT.append((label, []))
SCRIPT.append((label, list(actions)))
for kind, water in [('oak_boat', 'water'), ('birch_chest_boat', 'water'),
@@ -38,11 +39,12 @@ for kind, water in [('oak_boat', 'water'), ('birch_chest_boat', 'water'),
for n in range(3):
SCRIPT.append((f'{case}-surface-wait-{n}', []))
probe(case + '-surface', True)
SCRIPT.append((case + '-short-dip', [f'cmd Alice fill -3 81 -3 3 84 3 minecraft:{water}']))
probe(case + '-short', True)
# Each dip is one step (20 ticks, plus the ticks the client's syncs take) observed at its own end, far from
# the 60 that eject: observed a step later, a loaded machine's syncs pushed a short dip past 60 (2026-11-02).
probe(case + '-short', True, actions=[f'cmd Alice fill -3 81 -3 3 84 3 minecraft:{water}'])
SCRIPT.append((case + '-surface-reset', ['cmd Alice fill -3 81 -3 3 84 3 minecraft:air']))
SCRIPT.append((case + '-submerge', [f'cmd Alice fill -3 81 -3 3 84 3 minecraft:{water}']))
probe(case + '-reset', True)
probe(case + '-reset', True, actions=[f'cmd Alice fill -3 81 -3 3 84 3 minecraft:{water}'])
# 20 + 40 + 20 ticks under water by the last observation: past 60 however fast the steps go.
for n in range(2):
SCRIPT.append((f'{case}-underwater-wait-{n}', []))
probe(case + '-ejected', False)
+15 -3
View File
@@ -270,6 +270,10 @@ def sides(name):
return lock, build, vanilla_server, horizon_server, client
# How many times one_player runs vanilla's side before giving up on a run its `check` rejects.
VANILLA_ATTEMPTS = 3
def one_player(name, script, shown, check, scope, step_seconds=4.0):
"""A harness of one player, Alice: on each server she joins, then runs `script`, a list of (step, client
actions) that each last `step_seconds` of the server's time (20 ticks a second). The lines `shown(transcript)` keeps must match vanilla's exactly, and
@@ -308,7 +312,17 @@ def one_player(name, script, shown, check, scope, step_seconds=4.0):
return out.read_text(encoding='utf-8').splitlines()
world = generate_world(vanilla_server, build)
vanilla_all = run('vanilla', vanilla_server, world)
# Vanilla's run is the specification only when `check` accepts it. On a loaded machine a real-time step can
# miss its moment (a bobber that lands before the armor stand is there), so a run it rejects is made again,
# up to VANILLA_ATTEMPTS times, before Horizon's is compared with it; Horizon's run is never retried.
for attempt in range(1, VANILLA_ATTEMPTS + 1):
vanilla_all = run('vanilla', vanilla_server, world)
why = check(shown(vanilla_all), vanilla_all)
if why is None:
break
print(f'{name}: vanilla run {attempt} of {VANILLA_ATTEMPTS} is no proof ({why})', flush=True)
else:
raise AssertionError(f'{why}: the script did not run on vanilla in {VANILLA_ATTEMPTS} runs (see {build})')
horizon_all = run('horizon', horizon_server, world)
errors = [l for l in vanilla_all + horizon_all if ' error ' in f' {l} ' or 'disconnected' in l]
vanilla, horizon = shown(vanilla_all), shown(horizon_all)
@@ -317,8 +331,6 @@ def one_player(name, script, shown, check, scope, step_seconds=4.0):
diff = '\n'.join(difflib.unified_diff(vanilla, horizon, 'vanilla', 'horizon', n=1, lineterm=''))
raise AssertionError(f'{len(errors)} errors, transcripts differ (see {build}/vanilla.txt and horizon.txt):\n'
+ '\n'.join(errors[:10]) + '\n' + diff[:6000])
if (why := check(vanilla, vanilla_all)) is not None:
raise AssertionError(f'{why}: the script did not run (see {build})')
lab.write_json(build / 'report.json', dict(
schema=1, minecraft=lock['minecraft']['version'], platform=platform.platform(),
java=subprocess.check_output(['java', '--version'], text=True).splitlines()[0],