mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Redstone catalogue: Horizon probe and the parity harness
redstone_catalog_fixture builds the same contraptions in a map-backed horizon-world level, clicks the same levers and buttons (Behaviours::use_without_item) and runs tick_scheduled, run_block_events and tick_block_entities, printing what VanillaRedstoneCatalogOracle prints. tools/test_redstone_catalog_parity.py reads the catalogue, downloads the archive's schematics into .local/lra-catalog/ (pinned to da4dd51, one sha256 over all 188), turns each entry into a case (switches off except those the catalogue held on, a neighbour update on every block, then input steps: every vector up to 8 bits, random ones beyond, random changes for stateful builds) and compares both sides step by step. verified entries must match; partial and unverified ones are reported. Entries whose inputs are analog levels, lecterns or bare wire are listed as unsupported for now. Three shards; --full runs larger sweeps and every vector up to 16 bits. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
603584ef2e
commit
4d4872cedd
3 files changed
+573
No files matched your search
@@ -147,6 +147,7 @@ mod player_hurt_fixture;
|
||||
mod player_inventory_fixture;
|
||||
mod player_travel_fixture;
|
||||
mod portal_travel_fixture;
|
||||
mod redstone_catalog_fixture;
|
||||
mod registered_player_combat_fixture;
|
||||
mod registry_collision_scan_fixture;
|
||||
mod registry_fall_ray_fixture;
|
||||
@@ -320,6 +321,7 @@ probes! {
|
||||
player_inventory_fixture,
|
||||
player_travel_fixture,
|
||||
portal_travel_fixture,
|
||||
redstone_catalog_fixture,
|
||||
registered_player_combat_fixture,
|
||||
registry_collision_scan_fixture,
|
||||
registry_fall_ray_fixture,
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
//! Rust side of `tools/test_redstone_catalog_parity.py`: builds each Logical
|
||||
//! Redstone Archive contraption in a map-backed level, drives its levers and
|
||||
//! buttons like a player's click and runs whole level ticks (scheduled ticks,
|
||||
//! block events, moving pistons), printing what `VanillaRedstoneCatalogOracle`
|
||||
//! prints. Comparator levels of containers and lecterns come from the oracle
|
||||
//! (`analog`): nothing here moves items.
|
||||
use horizon_core::{
|
||||
geometry::{Direction, Vec3},
|
||||
position::{BlockPos, ChunkPos},
|
||||
random::LegacyRandom,
|
||||
};
|
||||
use horizon_world::{
|
||||
level::{Behaviours, LevelAccess, PlayerContext, Ticks, data::load_registry},
|
||||
registry::BlockStateId,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::Write as _,
|
||||
io::{BufRead, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
struct Cells {
|
||||
cells: HashMap<BlockPos, BlockStateId>,
|
||||
air: BlockStateId,
|
||||
random: LegacyRandom,
|
||||
time: i64,
|
||||
analog: HashMap<BlockPos, i32>,
|
||||
}
|
||||
impl LevelAccess for Cells {
|
||||
fn block_state(&self, pos: BlockPos) -> BlockStateId {
|
||||
self.cells.get(&pos).copied().unwrap_or(self.air)
|
||||
}
|
||||
fn set_raw(&mut self, pos: BlockPos, state: BlockStateId) -> Option<BlockStateId> {
|
||||
let old = if state == self.air {
|
||||
self.cells.remove(&pos)
|
||||
} else {
|
||||
self.cells.insert(pos, state)
|
||||
};
|
||||
Some(old.unwrap_or(self.air))
|
||||
}
|
||||
fn is_loaded(&self, _pos: BlockPos) -> bool {
|
||||
true
|
||||
}
|
||||
fn random(&mut self) -> &mut LegacyRandom {
|
||||
&mut self.random
|
||||
}
|
||||
fn game_time(&self) -> i64 {
|
||||
self.time
|
||||
}
|
||||
fn block_entity_analog_signal(
|
||||
&self,
|
||||
pos: BlockPos,
|
||||
_state: BlockStateId,
|
||||
_direction: Direction,
|
||||
) -> i32 {
|
||||
self.analog.get(&pos).copied().unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
/// The oracle's `mix`: one block's share of the box hash.
|
||||
fn mix(x: i32, y: i32, z: i32, value: u64) -> u64 {
|
||||
let mut k = (x as i64 as u64)
|
||||
.wrapping_mul(0x9E3779B97F4A7C15)
|
||||
.wrapping_add((y as i64 as u64).wrapping_mul(0xC2B2AE3D27D4EB4F))
|
||||
.wrapping_add((z as i64 as u64).wrapping_mul(0x165667B19E3779F9))
|
||||
.wrapping_add(value.wrapping_mul(0xD6E8FEB86659FD93));
|
||||
k = (k ^ (k >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
|
||||
k = (k ^ (k >> 27)).wrapping_mul(0x94D049BB133111EB);
|
||||
k ^ (k >> 31)
|
||||
}
|
||||
|
||||
fn ints(value: &Value) -> Vec<i64> {
|
||||
value
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|v| v.as_i64().unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn pos_of(c: &[i64]) -> BlockPos {
|
||||
BlockPos::new(c[0] as i32, c[1] as i32, c[2] as i32)
|
||||
}
|
||||
|
||||
/// What the registry says about a state: lever (and whether it is on), button, comparator.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Kind {
|
||||
Lever(bool),
|
||||
Button,
|
||||
Comparator,
|
||||
Other,
|
||||
}
|
||||
|
||||
struct Run<'a> {
|
||||
behaviours: &'a Behaviours,
|
||||
kinds: &'a [Kind],
|
||||
level: Cells,
|
||||
ticks: Ticks,
|
||||
outputs: Vec<BlockPos>,
|
||||
/// The hashed box: the schematic and a margin of 2.
|
||||
min: BlockPos,
|
||||
max: BlockPos,
|
||||
/// Ticks the contraption may still run (the oracle counts the same).
|
||||
left: i64,
|
||||
/// `HORIZON_CATALOG_TRACE`: every block at a step's end, as `# cell` lines.
|
||||
trace: bool,
|
||||
out: String,
|
||||
}
|
||||
impl Run<'_> {
|
||||
fn kind(&self, state: BlockStateId) -> Kind {
|
||||
self.kinds
|
||||
.get(state.0 as usize)
|
||||
.copied()
|
||||
.unwrap_or(Kind::Other)
|
||||
}
|
||||
fn comparator(&self, pos: BlockPos) -> Option<i32> {
|
||||
(self.kind(self.level.block_state(pos)) == Kind::Comparator).then(|| {
|
||||
self.ticks
|
||||
.comparator_outputs
|
||||
.get(&pos)
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
})
|
||||
}
|
||||
fn outputs(&self) -> String {
|
||||
let mut s = String::new();
|
||||
for (i, &pos) in self.outputs.iter().enumerate() {
|
||||
if i > 0 {
|
||||
s.push(',');
|
||||
}
|
||||
let _ = write!(s, "{}", self.level.block_state(pos).0);
|
||||
if let Some(out) = self.comparator(pos) {
|
||||
let _ = write!(s, "/{out}");
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
fn quiet(&self) -> bool {
|
||||
self.ticks.blocks.count() == 0
|
||||
&& self.ticks.fluids.count() == 0
|
||||
&& self.ticks.block_events.is_empty()
|
||||
&& self.ticks.moving_pistons.is_empty()
|
||||
}
|
||||
/// `ServerLevel.tick`'s block work until the level is quiet or `limit` ticks.
|
||||
fn ticks(&mut self, limit: i64) {
|
||||
let mut last = self.outputs();
|
||||
let _ = writeln!(self.out, "t 0 {last}");
|
||||
let mut t = 0;
|
||||
while t < limit && self.left > 0 && !self.quiet() {
|
||||
t += 1;
|
||||
self.left -= 1;
|
||||
self.level.time += 1;
|
||||
let b = self.behaviours;
|
||||
b.tick_scheduled(&mut self.level, &mut self.ticks);
|
||||
b.run_block_events(&mut self.level, &mut self.ticks);
|
||||
b.tick_block_entities(&mut self.level, &mut self.ticks);
|
||||
let now = self.outputs();
|
||||
if now != last {
|
||||
let _ = writeln!(self.out, "t {t} {now}");
|
||||
}
|
||||
last = now;
|
||||
}
|
||||
let mut hash = 0u64;
|
||||
for (&pos, &state) in &self.level.cells {
|
||||
let inside = (self.min.x..=self.max.x).contains(&pos.x)
|
||||
&& (self.min.y..=self.max.y).contains(&pos.y)
|
||||
&& (self.min.z..=self.max.z).contains(&pos.z);
|
||||
if inside {
|
||||
let extra = self.comparator(pos).map_or(0, |o| ((o + 1) as u64) << 32);
|
||||
let value = state.0 as u64 + extra;
|
||||
hash = hash.wrapping_add(mix(pos.x, pos.y, pos.z, value));
|
||||
if self.trace {
|
||||
let _ = writeln!(self.out, "# cell {},{},{} {value}", pos.x, pos.y, pos.z);
|
||||
}
|
||||
}
|
||||
}
|
||||
let quiet = u8::from(self.quiet());
|
||||
let _ = writeln!(self.out, "end {t} {quiet} {hash:x}");
|
||||
}
|
||||
fn act(&mut self, pos: BlockPos, on: bool) {
|
||||
let use_it = match self.kind(self.level.block_state(pos)) {
|
||||
Kind::Lever(powered) => powered != on,
|
||||
Kind::Button => on,
|
||||
_ => false,
|
||||
};
|
||||
if use_it {
|
||||
let hit = Vec3::new(pos.x as f64 + 0.5, pos.y as f64 + 0.5, pos.z as f64 + 0.5);
|
||||
let player = PlayerContext {
|
||||
yaw: 0.0,
|
||||
pitch: 0.0,
|
||||
may_build: true,
|
||||
can_eat: false,
|
||||
game_master: false,
|
||||
};
|
||||
self.behaviours.use_without_item(
|
||||
&mut self.level,
|
||||
&mut self.ticks,
|
||||
pos,
|
||||
Direction::Up,
|
||||
hit,
|
||||
player,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut args = crate::args().skip(1);
|
||||
let resources = args.next().expect("resource directory");
|
||||
let cases = args.next().expect("case file");
|
||||
let output = args.next().expect("output file");
|
||||
let resources = Path::new(&resources);
|
||||
let registry = load_registry(resources)?;
|
||||
let behaviours = Behaviours::load(®istry, resources)?;
|
||||
let air = registry.default_state("minecraft:air").unwrap();
|
||||
let kinds: Vec<Kind> = (0..registry.len() as u32)
|
||||
.map(|id| match registry.state(BlockStateId(id)) {
|
||||
Some(s) if s.name == "minecraft:lever" => {
|
||||
Kind::Lever(s.properties.get("powered").is_some_and(|v| v == "true"))
|
||||
}
|
||||
Some(s) if s.name.ends_with("_button") => Kind::Button,
|
||||
Some(s) if s.name == "minecraft:comparator" => Kind::Comparator,
|
||||
_ => Kind::Other,
|
||||
})
|
||||
.collect();
|
||||
let mut out = std::io::BufWriter::new(std::fs::File::create(output)?);
|
||||
for line in std::io::BufReader::new(std::fs::File::open(cases)?).lines() {
|
||||
let case: Value = serde_json::from_str(&line?)?;
|
||||
let size = ints(&case["size"]);
|
||||
let mut ticks = Ticks::new(0);
|
||||
for x in -1..=(size[0] as i32 >> 4) + 1 {
|
||||
for z in -1..=(size[2] as i32 >> 4) + 1 {
|
||||
ticks.add_empty_chunk(ChunkPos::new(x, z))?;
|
||||
}
|
||||
}
|
||||
let cells: Vec<Vec<i64>> = case["cells"].as_array().unwrap().iter().map(ints).collect();
|
||||
let mut level = Cells {
|
||||
cells: HashMap::new(),
|
||||
air,
|
||||
random: LegacyRandom::new(0),
|
||||
time: case["time"].as_i64().unwrap(),
|
||||
analog: HashMap::new(),
|
||||
};
|
||||
for c in &cells {
|
||||
level.set_raw(pos_of(c), BlockStateId(c[3] as u32));
|
||||
}
|
||||
for c in case["comparators"].as_array().unwrap().iter().map(ints) {
|
||||
ticks.comparator_outputs.insert(pos_of(&c), c[3] as i32);
|
||||
}
|
||||
for c in case["analog"].as_array().unwrap().iter().map(ints) {
|
||||
level.analog.insert(pos_of(&c), c[3] as i32);
|
||||
}
|
||||
let outputs = case["outputs"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|v| pos_of(&ints(v)))
|
||||
.collect();
|
||||
let mut run = Run {
|
||||
behaviours: &behaviours,
|
||||
kinds: &kinds,
|
||||
level,
|
||||
ticks,
|
||||
outputs,
|
||||
min: BlockPos::new(-2, -2, -2),
|
||||
max: BlockPos::new(size[0] as i32 + 1, size[1] as i32 + 1, size[2] as i32 + 1),
|
||||
left: case["budget"].as_i64().unwrap(),
|
||||
trace: std::env::var_os("HORIZON_CATALOG_TRACE").is_some(),
|
||||
out: String::new(),
|
||||
};
|
||||
let _ = writeln!(run.out, "case {}", case["code"].as_str().unwrap());
|
||||
for c in &cells {
|
||||
let pos = pos_of(c);
|
||||
let block = behaviours.block(run.level.block_state(pos));
|
||||
behaviours.handle_neighbor_changed(&mut run.level, &mut run.ticks, pos, block);
|
||||
}
|
||||
run.out.push_str("step init\n");
|
||||
run.ticks(case["settle"].as_i64().unwrap());
|
||||
let limit = case["max"].as_i64().unwrap();
|
||||
for (index, step) in case["steps"].as_array().unwrap().iter().enumerate() {
|
||||
if run.left <= 0 {
|
||||
run.out.push_str("budget spent\n");
|
||||
break;
|
||||
}
|
||||
let _ = writeln!(run.out, "step {index}");
|
||||
for act in step.as_array().unwrap().iter().map(ints) {
|
||||
run.act(pos_of(&act), act[3] != 0);
|
||||
}
|
||||
run.ticks(limit);
|
||||
}
|
||||
out.write_all(run.out.as_bytes())?;
|
||||
out.flush()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the Logical Redstone Archive's contraptions in vanilla and in Horizon, tick by tick.
|
||||
|
||||
The catalogue (`tools/lra-catalog/`, by maybeachromium, WTFPL) lists each archive entry's input and output
|
||||
positions and what it was checked against. The archive's schematics have no licence and are never committed:
|
||||
they are downloaded into `.local/lra-catalog/` and checked against the digest pinned here.
|
||||
|
||||
Each contraption is built from its schematic in `VanillaRedstoneCatalogOracle` (a real flat-world server)
|
||||
and in `redstone_catalog_fixture` (horizon-world). Switches start off (except those the catalogue held on),
|
||||
every block gets a neighbour update, and the level runs until quiet. Then each step sets the input levers
|
||||
and presses buttons as a player's click would, and runs whole level ticks until quiet. Both sides print the
|
||||
outputs that changed after every tick, the tick the step ended on, and a hash of every block in the box.
|
||||
|
||||
`verified` entries must match; `partial` and `unverified` ones are reported (expected unknown). Inputs the
|
||||
fixture can't drive yet (analog levels, lecterns, bare wire) are listed as unsupported.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
try:
|
||||
from . import lab, lra_schem, test_rust_parity as base
|
||||
except ImportError:
|
||||
import lab
|
||||
import lra_schem
|
||||
import test_rust_parity as base
|
||||
|
||||
ARCHIVE = 'mattbatwings/Logical-Redstone-Archive'
|
||||
COMMIT = 'da4dd515559458140f78a555600e7e4607ac6b8c'
|
||||
CATALOG = lab.ROOT / 'tools/lra-catalog/entries.jsonl'
|
||||
# sha256 of the sorted "<path> <sha256>" lines of every entry's schematic at COMMIT.
|
||||
SCHEMATICS_SHA256 = 'a02f086c05b4280605db0c992867632c4c5f0f91fc4648a4933a4850f70ed19c'
|
||||
DATA = lab.LOCAL / 'lra-catalog'
|
||||
DRIVEN = {'lever', 'stone_button', 'oak_button', 'birch_button'}
|
||||
# tools/parity.py runs NAME#1..#3 in parallel (PARITY_SHARD=i/3), each with every third contraption.
|
||||
PARITY_SHARDS = 3
|
||||
|
||||
|
||||
def run(command, full, **kwargs):
|
||||
"""test_rust_parity.run, without its 15-minute hang guard for --full."""
|
||||
if not full:
|
||||
return base.run(command, **kwargs)
|
||||
subprocess.run(command, cwd=lab.ROOT, check=True, **kwargs)
|
||||
|
||||
|
||||
def load_catalog():
|
||||
return [json.loads(line) for line in CATALOG.read_text(encoding='utf-8').splitlines() if line]
|
||||
|
||||
|
||||
def fetch_schematics(entries):
|
||||
"""Each entry's schematic at COMMIT, under DATA/schematics; all of them checked against SCHEMATICS_SHA256."""
|
||||
root = DATA / 'schematics'
|
||||
sums = {}
|
||||
for entry in entries:
|
||||
rel = entry.get('schematic')
|
||||
if not rel or entry['status'] == 'no_schematic':
|
||||
continue
|
||||
if '..' in Path(rel).parts or rel.startswith('/'):
|
||||
raise ValueError(f'bad schematic path {rel}')
|
||||
path = root / rel
|
||||
if not path.exists():
|
||||
url = f'https://raw.githubusercontent.com/{ARCHIVE}/{COMMIT}/' + urllib.parse.quote(rel)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
request = urllib.request.Request(url, headers={'User-Agent': 'horizon/0.1'})
|
||||
with urllib.request.urlopen(request, timeout=120) as response:
|
||||
data = response.read()
|
||||
path.with_suffix('.part').write_bytes(data)
|
||||
path.with_suffix('.part').replace(path)
|
||||
sums[rel] = lab.digest(path, 'sha256')
|
||||
listing = ''.join(f'{rel} {sums[rel]}\n' for rel in sorted(sums))
|
||||
combined = hashlib.sha256(listing.encode()).hexdigest()
|
||||
if combined != SCHEMATICS_SHA256:
|
||||
(DATA / 'schematics.sha256').write_text(listing)
|
||||
raise SystemExit(f'schematics digest {combined} is not the pinned {SCHEMATICS_SHA256}: see {DATA / "schematics.sha256"}')
|
||||
return root
|
||||
|
||||
|
||||
class States:
|
||||
"""Schematic state strings -> vanilla state ids (`facts.json`), missing properties at their defaults."""
|
||||
def __init__(self, resources):
|
||||
facts = json.loads((resources / 'facts.json').read_text())
|
||||
self.ids, self.props = {}, []
|
||||
for index, state in enumerate(facts['states']):
|
||||
name, props = lra_schem.parse_state(state[3].replace('Block{', '').replace('}', '', 1))
|
||||
self.ids[(name, tuple(sorted(props.items())))] = index
|
||||
self.props.append((name, props))
|
||||
self.defaults = facts['defaults']
|
||||
|
||||
def id(self, text, **override):
|
||||
name, props = lra_schem.parse_state(text)
|
||||
full = dict(self.props[self.defaults[name]][1])
|
||||
full.update({k: v for k, v in props.items() if k in full})
|
||||
full.update(override)
|
||||
return self.ids[(name, tuple(sorted(full.items())))]
|
||||
|
||||
|
||||
def held_on(entry):
|
||||
"""Switches the catalogue's runs held on (`other_switches`), as positions; 'as saved' keeps saved ones."""
|
||||
text = ' '.join(str(v.get('other_switches', 'off')) for v in entry['verified'] or [])
|
||||
held = {tuple(map(int, m)) for m in re.findall(r'\[(\d+), (\d+), (\d+)', text)}
|
||||
saved = re.search(r'at x=(\d+), y=(\d+) on \(as saved\)', text)
|
||||
return held, (tuple(map(int, saved.groups())) if saved else None)
|
||||
|
||||
|
||||
def steps(entry, bits, full):
|
||||
"""Input vectors: every one when few enough (or random ones), random changes for stateful builds."""
|
||||
rng = random.Random(entry['code'])
|
||||
groups = [[tuple(p) for p in g['positions']] for g in entry['inputs']]
|
||||
sequence = any('sequence' in v['method'] for v in entry['verified'] or [])
|
||||
if sequence:
|
||||
out = []
|
||||
for _ in range(512 if full else 24):
|
||||
group = rng.choice(groups)
|
||||
out.append([[*p, rng.randrange(2)] for p in group])
|
||||
return out
|
||||
exhaustive = len(bits) <= (16 if full else 8)
|
||||
vectors = range(1 << len(bits)) if exhaustive else [0, (1 << len(bits)) - 1] + [rng.getrandbits(len(bits)) for _ in range(1024 if full else 24)]
|
||||
return [[[*p, (v >> i) & 1] for i, p in enumerate(bits)] for v in vectors]
|
||||
|
||||
|
||||
def build(entry, schematics, states, full):
|
||||
"""The case both sides run, or the reason it can't be run."""
|
||||
kinds = {b for g in entry['inputs'] for b in g['blocks']}
|
||||
if not kinds:
|
||||
return None, 'no inputs mapped'
|
||||
if not kinds <= DRIVEN or any(g['encoding'] != 'binary' for g in entry['inputs']):
|
||||
return None, 'unsupported inputs: ' + ','.join(sorted(kinds - DRIVEN) or ['analog levels'])
|
||||
schem = lra_schem.read(schematics / entry['schematic'])
|
||||
held, saved_row = held_on(entry)
|
||||
cells, comparators, containers, lecterns = [], [], [], []
|
||||
for x, y, z, text in schem['cells']:
|
||||
name, props = lra_schem.parse_state(text)
|
||||
override = {}
|
||||
if name == 'minecraft:lever' or name.endswith('_button'):
|
||||
keep = name == 'minecraft:lever' and saved_row and (x, y) == saved_row and props.get('powered') == 'true'
|
||||
override['powered'] = 'true' if (x, y, z) in held or keep else 'false'
|
||||
cells.append([x, y, z, states.id(text, **override)])
|
||||
for be in schem['block_entities']:
|
||||
x, y, z = be['pos']
|
||||
data = be['data']
|
||||
if 'OutputSignal' in data:
|
||||
comparators.append([x, y, z, data['OutputSignal']])
|
||||
if isinstance(data.get('Items'), list):
|
||||
items = [[i.get('Slot', 0), i.get('id', 'minecraft:air'), i.get('count', i.get('Count', 1))] for i in data['Items']]
|
||||
containers.append([x, y, z, items])
|
||||
if isinstance(data.get('Book'), dict):
|
||||
book = data['Book']
|
||||
pages = book.get('components', {}).get('minecraft:written_book_content', book.get('components', {}).get('minecraft:writable_book_content', {})).get('pages') or book.get('tag', {}).get('pages') or []
|
||||
lecterns.append([x, y, z, len(pages), data.get('Page', 0)])
|
||||
bits = [tuple(p) for g in entry['inputs'] for p in g['positions']]
|
||||
outputs = [p for g in entry['outputs'] for p in g['positions']]
|
||||
return {'code': entry['code'], 'size': schem['size'], 'cells': cells, 'comparators': comparators, 'containers': containers,
|
||||
'lecterns': lecterns, 'outputs': outputs, 'settle': 600, 'max': 300, 'budget': 60000 if full else 2500,
|
||||
'steps': steps(entry, bits, full)}, None
|
||||
|
||||
|
||||
def split(text):
|
||||
"""Output text -> {code: (lines compared, analog lines, other lines)}."""
|
||||
out, code = {}, None
|
||||
for line in text.splitlines():
|
||||
if line.startswith('case '):
|
||||
code = line[5:]
|
||||
out[code] = ([], [], [])
|
||||
elif line.startswith('# '):
|
||||
continue
|
||||
elif line.startswith('analog '):
|
||||
out[code][1].append(line[7:])
|
||||
elif line.startswith(('drift ', 'error ', 'time ')):
|
||||
out[code][2].append(line)
|
||||
else:
|
||||
out[code][0].append(line)
|
||||
return out
|
||||
|
||||
|
||||
def first_difference(a, b):
|
||||
for i, (x, y) in enumerate(zip(a, b)):
|
||||
if x != y:
|
||||
return i
|
||||
return min(len(a), len(b))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
parser.add_argument('--full', action='store_true', help='every entry, larger sweeps (exhaustive up to 16 bits)')
|
||||
parser.add_argument('--only', help='comma separated codes')
|
||||
args = parser.parse_args()
|
||||
lock, jars, libraries = base.verify_artifacts()
|
||||
resources = lab.LOCAL / 'horizon-client' / lock['minecraft']['version']
|
||||
shard, shards = map(int, os.environ.get('PARITY_SHARD', '1/1').split('/'))
|
||||
work = lab.LOCAL / 'redstone-catalog-parity' / (f'shard-{shard}' if shards > 1 else 'all')
|
||||
work.mkdir(parents=True, exist_ok=True)
|
||||
entries = load_catalog()
|
||||
schematics = fetch_schematics(entries)
|
||||
entries = [e for e in entries if not args.only or e['code'] in args.only.split(',')][shard - 1::shards]
|
||||
states = States(resources)
|
||||
cases, skipped = [], {}
|
||||
for entry in entries:
|
||||
if entry['status'] == 'no_schematic':
|
||||
continue
|
||||
case, why = build(entry, schematics, states, args.full)
|
||||
if case:
|
||||
cases.append(case)
|
||||
else:
|
||||
skipped[entry['code']] = why
|
||||
(work / 'vanilla-cases.jsonl').write_text(''.join(json.dumps(c, separators=(',', ':')) + '\n' for c in cases))
|
||||
classes = work / 'classes'
|
||||
classes.mkdir(exist_ok=True)
|
||||
deps = os.pathsep.join(map(str, [jars['server'], *libraries]))
|
||||
java_root = lab.ROOT / 'mods/horizon/src/test/java'
|
||||
oracle = java_root / 'dev/horizonlab/parity/VanillaRedstoneCatalogOracle.java'
|
||||
started = time.time()
|
||||
run(['javac', '--release', str(lock['minecraft']['java_major']), '-proc:none', '-nowarn', '-sourcepath', str(java_root),
|
||||
'-cp', deps, '-d', str(classes), str(oracle)], args.full)
|
||||
run(['java', '-Xmx4G', '-cp', str(classes) + os.pathsep + deps, 'dev.horizonlab.parity.VanillaRedstoneCatalogOracle',
|
||||
str(work / 'vanilla-cases.jsonl'), str(work / 'vanilla.txt')], args.full, stdout=subprocess.DEVNULL)
|
||||
vanilla = split((work / 'vanilla.txt').read_text())
|
||||
for case in cases:
|
||||
notes = vanilla.get(case['code'], ([], [], []))[2]
|
||||
case['time'] = next((int(n[5:]) for n in notes if n.startswith('time ')), 0)
|
||||
case['analog'] = [[*map(int, a.split(' ')[0].split(',')), int(a.split(' ')[1])] for a in vanilla.get(case['code'], ([], [], []))[1]]
|
||||
(work / 'rust-cases.jsonl').write_text(''.join(json.dumps(c, separators=(',', ':')) + '\n' for c in cases))
|
||||
run(['cargo', 'build', '--release', '-p', 'horizon-world', '--example', 'world_probe'], args.full)
|
||||
probe = lab.cargo_target() / 'release/examples' / ('world_probe.exe' if os.name == 'nt' else 'world_probe')
|
||||
run([str(probe), 'redstone_catalog_fixture', str(resources), str(work / 'rust-cases.jsonl'), str(work / 'rust.txt')], args.full)
|
||||
rust = split((work / 'rust.txt').read_text())
|
||||
status = {e['code']: e['status'] for e in entries}
|
||||
report, failures = {}, []
|
||||
for case in cases:
|
||||
code = case['code']
|
||||
a, _, notes = vanilla.get(code, ([], [], ['error missing']))
|
||||
b = rust.get(code, ([], [], []))[0]
|
||||
notes = [n for n in notes if not n.startswith('time ')]
|
||||
if any(n.startswith('error timeout') for n in notes):
|
||||
result = {'result': 'oracle timeout', 'notes': notes, 'why': 'vanilla took over 4 minutes: not compared'}
|
||||
elif any(n.startswith('error') for n in notes):
|
||||
result = {'result': 'oracle error', 'notes': notes}
|
||||
elif any(n.startswith('drift') for n in notes):
|
||||
result = {'result': 'not comparable', 'notes': notes[:5], 'why': 'vanilla moved items: container levels changed'}
|
||||
elif a == b:
|
||||
result = {'result': 'match', 'steps': sum(line.startswith('step') for line in a)}
|
||||
else:
|
||||
i = first_difference(a, b)
|
||||
step = next((line for line in reversed(a[:i + 1]) if line.startswith('step')), '?')
|
||||
result = {'result': 'mismatch', 'at': step, 'vanilla': a[i:i + 4], 'horizon': b[i:i + 4]}
|
||||
result['status'] = status[code]
|
||||
report[code] = result
|
||||
if status[code] == 'verified' and result['result'] in ('mismatch', 'oracle error'):
|
||||
failures.append(code)
|
||||
for code, why in skipped.items():
|
||||
report[code] = {'result': 'skipped', 'why': why, 'status': status[code]}
|
||||
lab.write_json(work / 'report.json', report)
|
||||
counts = {}
|
||||
for r in report.values():
|
||||
counts.setdefault((r['status'], r['result']), 0)
|
||||
counts[(r['status'], r['result'])] += 1
|
||||
for (st, res), n in sorted(counts.items()):
|
||||
print(f'{st:>10} {res}: {n}')
|
||||
for code in sorted(report):
|
||||
r = report[code]
|
||||
if r['result'] == 'mismatch':
|
||||
print(f'{code} ({r["status"]}): first difference in {r["at"]}\n vanilla {r["vanilla"]}\n horizon {r["horizon"]}')
|
||||
print(f'{len(cases)} contraptions run in {time.time() - started:.0f} s; report: {work / "report.json"}')
|
||||
raise SystemExit(1 if failures else 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in new issue
Block a user