Skip to main content

delvewright_dsl/
metrics.rs

1//! The metrics standard (spec-0049 §2) — pipeline stage 0.
2//!
3//! One machine-readable table, engine-owned data, in two halves whose epistemic
4//! status differs and is recorded per entry:
5//!
6//! * **Player metrics** — facts of pinned Minecraft Java 1.21.11. Not chosen,
7//!   so not calibratable: walking a level cannot make a player 0.7 blocks wide.
8//! * **Building metrics** — standards this project fixes. Every one carries
9//!   [`BuildingEntry::calibrated`], `false` until the metrics gym's walk
10//!   (spec-0049 §2.3) rules on it.
11//!
12//! # Why this module is in `delvewright-dsl`
13//!
14//! It is the crate every other one already reaches: `schem`, `grammar`, `admit`,
15//! `compiler` and `render` all resolve it. A metrics module that the navigation
16//! model imports but the render layer cannot would be one authority for some
17//! consumers and a copy for the rest, which is the shape this table exists to
18//! end. It is also the crate that owns [`crate::diagnostic`], and the stage-3
19//! and stage-4 documents whose names resolve into this table are validated here.
20//!
21//! # One authority, structurally
22//!
23//! The player half is not a second table that agrees with the navigation model —
24//! it **is** the navigation model's constants. `compiler::nav` imports
25//! [`MAX_AUTO_STEP_16`], [`MAX_JUMP_RISE_16`] and [`FULL_16`] from here;
26//! `compiler::crosshair`, `compiler::render_plan`, `compiler::view::viewer` and
27//! `compiler::combat` import the body constants they used to declare. There is
28//! one definition of each number in the workspace and the export re-serializes
29//! it, so a player metric cannot drift from the model that proves routes.
30//!
31//! What that replaced is worth recording, because it is the defect this table
32//! was written against rather than a hypothetical: the player eye height was
33//! declared four times (`compiler::render_plan`, `compiler::view::viewer`,
34//! `compiler::creator` in milli-blocks, `render::occupancy` in `f32`) and the
35//! body width twice, each a literal `0.6` or `1.62` with its own doc comment
36//! saying it was vanilla's. Nothing related them, so nothing could have gone red
37//! had one moved.
38//!
39//! # Provenance is recorded per entry
40//!
41//! [`Provenance`] says where a number came from, and the four values are
42//! deliberately not interchangeable: an [`Provenance::EngineConstant`] cannot
43//! drift because there is nothing to drift from, a [`Provenance::VanillaRule`] is
44//! a claim about the game this repository has **not** measured on a running
45//! server, a [`Provenance::Derived`] carries its arithmetic in its note, and a
46//! [`Provenance::Provisional`] is a seed for the gym and is not a standard yet.
47//! Dressing the last as one of the first three is the failure this field exists
48//! to make impossible to commit silently.
49//!
50//! # A provisional value cannot be consumed quietly
51//!
52//! [`BuildingEntry::value`] is the only way to read a building metric's number,
53//! and it takes `&mut `[`Reads`]. So a verdict that rests on an uncalibrated
54//! standard has, by construction, recorded that it did, and [`Metrics::notice`]
55//! turns that ledger into `DW0813`. The obligation lives in the signature, not
56//! in a line of documentation somebody has to remember.
57//!
58//! The residual, named rather than implied: a caller that constructs its own
59//! [`Reads`], reads through it and drops it has bypassed the notice. That is a
60//! deliberate act and not the omission the rule exists to catch — nothing
61//! *forgets* to thread a ledger it had to construct.
62//!
63//! On the campaign path it is closed rather than merely narrow.
64//! [`crate::validate::validate_campaign_with`] constructs **one** ledger and
65//! threads it through the stage-3 and stage-4 checks together, so every building
66//! metric either of them rests a verdict on lands in the ledger the notice
67//! reads. There is no second ledger for a read to disappear into.
68
69use std::collections::{BTreeMap, BTreeSet};
70
71use serde::Serialize;
72
73use crate::diagnostic::{Diagnostic, DwCode, ExitTier};
74
75/// `DW0812`: a document names a metrics entry the table does not define — a
76/// `size_class`, an `opening` or a `pitch` that resolves to nothing.
77pub const DW_METRIC_UNKNOWN: DwCode = DwCode::new("DW0812", ExitTier::Build);
78
79/// `DW0813`: a verdict rests on a standard the gym has not walked.
80///
81/// This rule asks the campaign for nothing at all. It reports a property of the
82/// ENGINE's own table — that some number a check just used is a seed rather than
83/// a standard — so no campaign could adopt its way out of it. It is a warning
84/// (exit 0) for the same reason: a provisional number is still a number,
85/// the check still refuses, and what the line adds is that the green rests on
86/// something nobody has walked.
87pub const DW_METRIC_PROVISIONAL: DwCode = DwCode::new("DW0813", ExitTier::Build).about_the_engine();
88
89// ---------------------------------------------------------------------------
90// The player half — the one definition of each constant in this workspace.
91// ---------------------------------------------------------------------------
92
93/// The table's own revision. Bumped whenever the exported JSON changes by a
94/// single byte **from a version that has merged**, which
95/// `crates/dsl/tests/metrics.rs` enforces against a committed digest: a consumer
96/// that pins a version is pinning values, and values that move under a fixed
97/// version are the drift the pin was bought to prevent.
98///
99/// The qualifier is a scope, not an escape hatch, and the difference is worth
100/// the sentence: the harm exists only where something could already have pinned
101/// the number, so a branch still authoring the version it introduces has nothing
102/// to break. It is also not a claim anybody makes about their own change —
103/// whether a version has merged is a fact of `origin/main`.
104///
105/// No document declares a metrics version and no surface is gated by one. What
106/// it needs is that the number cannot stand still while the table moves, and
107/// that is the digest test.
108pub const METRICS_VERSION: u32 = 3;
109
110/// Player collision-box width in blocks (`0.6 × 0.6 × 1.8` standing).
111pub const PLAYER_WIDTH: f64 = 0.6;
112
113/// Player collision-box height in blocks, standing.
114pub const PLAYER_HEIGHT: f64 = 1.8;
115
116/// Player collision-box height in blocks, crouched.
117pub const PLAYER_CROUCHED_HEIGHT: f64 = 1.5;
118
119/// Player eye height above the floor of the cell the body stands in.
120pub const PLAYER_EYE_HEIGHT: f64 = 1.62;
121
122/// Player maximum health, in half-heart damage points.
123pub const PLAYER_MAX_HEALTH: f64 = 20.0;
124
125/// The largest rise, in sixteenths of a block, a walker crosses **without
126/// jumping** — vanilla's player `maxUpStep` is 0.6 blocks, and 9/16 = 0.5625 is
127/// the largest sixteenth under it (10/16 = 0.625 already needs a jump). A rise
128/// within this budget needs no headroom above the *source* cell: the player walks
129/// straight up onto a slab or a path edge.
130pub const MAX_AUTO_STEP_16: i64 = 9;
131
132/// The largest rise a walker can reach **by jumping**, in sixteenths. A vanilla
133/// player's jump apex is ≈1.2522 blocks, so a surface 20/16 = 1.25 up is
134/// reachable and 21/16 = 1.3125 is not. This is the bound that makes the
135/// **1.5-block** slab-to-full-block step-up the impossible move it is.
136pub const MAX_JUMP_RISE_16: i64 = 20;
137
138/// A full block's height in sixteenths.
139pub const FULL_16: i64 = 16;
140
141// One number, one definition: a full block is 16/16 here and in the collision
142// table this feeds, and this refuses to compile the day the two drift. It is
143// asserted on this side because `blockshape` must compile knowing nothing about
144// the crate around it — the prefab generators are a separate workspace and reach
145// it through `prefab-invariants`, which re-exports it.
146const _: () = assert!(crate::blockshape::FULL_HEIGHT_16 as i64 == FULL_16);
147
148/// What a walker has to do to gain a given rise — the engine's ONE answer, and
149/// the reason it is here rather than in either walk.
150///
151/// Two walks ask this question of two different object classes. The compiler's
152/// navigation model asks it of an assembled world, where a floor has a real
153/// collision top and a body has a footprint; `delvec::schem`'s walk asks it of a
154/// box of cells with a passability answer for each, which is what a grammar
155/// expansion, a structure template read off disk and a reassembled zone all are.
156/// The rule belongs to neither walk: it is a fact about the pinned game's body,
157/// and this crate is where the three numbers it is written in terms of already
158/// live — so the rule lives beside them, and both walks read one answer.
159///
160/// The two callers still measure the rise differently, and that is a difference
161/// of **measurement**, never of rule: a box of cells with no collision heights
162/// can only read a rise as whole cells, which over-states every partial-block
163/// step and therefore only ever refuses. The rise is the input; this is the rule.
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub enum Rise {
166    /// Inside [`MAX_AUTO_STEP_16`]: the body walks straight up, never leaves the
167    /// ground, and needs no room over its head.
168    Walk,
169    /// Past the auto-step budget and inside [`MAX_JUMP_RISE_16`]: the body jumps,
170    /// so the cell its head sweeps through must be clear or it head-bonks.
171    Jump,
172    /// Past the jump apex. No player makes this step.
173    Beyond,
174}
175
176/// Classify a rise between two standing surfaces, in sixteenths. Negative and
177/// zero rises are [`Rise::Walk`] — stepping down and walking level ask nothing
178/// of the ceiling.
179#[must_use]
180pub fn classify_rise_16(rise_16: i64) -> Rise {
181    if rise_16 <= MAX_AUTO_STEP_16 {
182        Rise::Walk
183    } else if rise_16 <= MAX_JUMP_RISE_16 {
184        Rise::Jump
185    } else {
186        Rise::Beyond
187    }
188}
189
190/// Can a body make this step? `head_clear` answers *is the cell the head sweeps
191/// through clear at the source* — it is consulted only for a [`Rise::Jump`], so a
192/// caller may compute it lazily.
193///
194/// This predicate is the whole of the step rule. A walk that answers "can a body
195/// get from here to there" without going through it is a second opinion, and the
196/// two this engine had disagreed by exactly this term: one of them omitted the
197/// head sweep, so it connected a full-block rise under a two-course ceiling that
198/// the other refuses, and the gate that admits a prefab proved its **positive**
199/// reachability claim over the looser of the two.
200#[must_use]
201pub fn step_allowed(rise_16: i64, head_clear: impl FnOnce() -> bool) -> bool {
202    match classify_rise_16(rise_16) {
203        Rise::Walk => true,
204        Rise::Jump => head_clear(),
205        Rise::Beyond => false,
206    }
207}
208
209/// **How far a body can jump**, per rise: for each whole-block rise from `+1`
210/// down to the deepest survivable fall, the widest gap of air columns a body
211/// crosses along one cardinal axis and lands, by any control it has — walking
212/// or sprinting, the jump pressed on any tick — from a runway no longer than
213/// its own launch cell.
214///
215/// Measured, not derived: `tools/spike-jump-arc/simulate.mjs` drives the
216/// pinned game's movement code (prismarine-physics, the stack the harness bot
217/// and the live jump spike both run) over the live spike's rig with the runway
218/// as a variable, takes the minimum over runways of 1, 2, 3, 4, 6 and 10
219/// cells, and its `--check` mode reproduces every landed/missed verdict of the
220/// live spike (`docs/notes/jump-arc-model.md` §2, 14 of 14). The live spike's
221/// narrower figures (sprint 3 flat, 2 at `+1`) are what a bot does **on cue**
222/// with its jump pressed before the edge; these are what a body **can** do with
223/// the jump pressed on the last supported tick, which is the question "can a
224/// player get in there" asks.
225///
226/// Monotone: a deeper landing never admits a narrower gap.
227pub const JUMP_REACH: [(i64, u32); 24] = [
228    (1, 3),
229    (0, 4),
230    (-1, 4),
231    (-2, 4),
232    (-3, 5),
233    (-4, 5),
234    (-5, 6),
235    (-6, 6),
236    (-7, 6),
237    (-8, 6),
238    (-9, 7),
239    (-10, 7),
240    (-11, 7),
241    (-12, 7),
242    (-13, 7),
243    (-14, 8),
244    (-15, 8),
245    (-16, 8),
246    (-17, 8),
247    (-18, 9),
248    (-19, 9),
249    (-20, 9),
250    (-21, 9),
251    (-22, 9),
252];
253
254/// **How high a body afloat climbs out**, in whole cells above the top water
255/// cell it floats in: a ledge whose standing cell is at most this far above that
256/// cell is one a swimmer pressing into it gets onto; one higher is a wall.
257///
258/// Measured with the same instrument as [`JUMP_REACH`]:
259/// `tools/spike-jump-arc/simulate.mjs --water` floats a body in a three-deep
260/// pool of source water beside a ledge and holds forward and jump, walking and
261/// sprinting. A ledge standing one cell over the top water cell (its top block
262/// flush with the water's) is climbed; two cells over is not. What lifts the
263/// body is the water branch of the player's travel: pressed into a wall with
264/// room 0.6 above, it gets a 0.3 upward impulse (`outOfLiquidImpulse` in the
265/// harness's physics). It is the same relationship an `ocean` world's walk
266/// plane keeps with its sea, one block above it.
267pub const WATER_CLIMB_OUT_RISE: i32 = 1;
268
269/// The widest gap [`JUMP_REACH`] admits for a rise measured in sixteenths, or
270/// `None` when no jump makes it: past [`MAX_JUMP_RISE_16`] upward, or deeper than
271/// [`unarmoured_survivable_fall_blocks`] downward (a body that lands there is
272/// dead, and a dead body is not standing anywhere).
273///
274/// A rise between two table rows reads the **shallower** row — a positive
275/// partial rise reads `+1`, a drop of 1.5 reads `-1` — which admits the smaller
276/// gap, so a partial block can only ever narrow what this says a body reaches.
277#[must_use]
278pub fn jump_max_gap(rise_16: i64) -> Option<u32> {
279    if rise_16 > MAX_JUMP_RISE_16 {
280        return None;
281    }
282    if rise_16 < -(unarmoured_survivable_fall_blocks() as i64) * FULL_16 {
283        return None;
284    }
285    // Round toward zero rise: +0.25 reads +1, -1.5 reads -1.
286    let row = if rise_16 > 0 {
287        1
288    } else {
289        -((-rise_16) / FULL_16)
290    };
291    JUMP_REACH.iter().find(|(r, _)| *r == row).map(|(_, g)| *g)
292}
293
294/// **Does the selector volume of the inclusive block box `lo..=hi` reach a body
295/// standing anywhere in `cell`?**
296///
297/// Vanilla's `@a[x=lo.x,dx=hi.x-lo.x,…]` builds the AABB `[lo, hi + 1]` on each
298/// axis — `EntitySelectorParser::createAabb` adds the block's own extent on the
299/// positive side, which is why `dx=0` still selects a whole block column — and
300/// keeps every entity whose own box **intersects** it. A standing body is
301/// [`PLAYER_WIDTH`] square about its position and [`PLAYER_HEIGHT`] tall from its
302/// feet, so a volume matches bodies whose FEET CELL it does not contain: half a
303/// width past every horizontal face, and a course below the bottom layer where
304/// only the head is inside.
305///
306/// This is the **generous** reading of that test — *could any body standing in
307/// this cell be matched* — because every caller uses it to keep a body OUT. A
308/// cell it clears is one no body standing in it can be selected from; a cell it
309/// reports is one some body standing in it can. Horizontally the whole cell of
310/// positions counts, since nothing makes a walking player stop at a cell centre
311/// and a pathfinder routinely parks one flush against a face.
312///
313/// The comparison is non-strict where vanilla's `AABB.intersects` is strict, so
314/// a body that exactly TOUCHES a face counts here and does not on the server.
315/// That is the same hair of generosity `bodyInVolume` takes in the harness, kept
316/// identical on purpose so the two sides cannot answer differently about a cell,
317/// and it costs one layer: a body whose feet sit exactly on the volume's ceiling
318/// is reported and is not really killed. Generous is the safe direction for a
319/// rule that keeps bodies out, and being the SAME on both sides matters more
320/// than the layer.
321///
322/// A consequence worth stating rather than leaving to be discovered: horizontally
323/// the shell is one cell wide for any body narrower than two blocks, so
324/// [`PLAYER_WIDTH`] does not decide it — the sweep over the cell's own positions
325/// does. [`PLAYER_HEIGHT`] does decide the vertical shell.
326///
327/// The engine's other reader of the same vanilla rule is
328/// `compiler::reach::ReachCompletion::possibly_completes_from`, which asks it of
329/// one stated body position rather than of a cell of them; the harness's is
330/// `bodyInVolume` in `harness/src/death-loop.ts`.
331///
332/// Here rather than in `delvec` for the reason [`step_allowed`] is here: the body
333/// is this table's, and a second copy of the arithmetic beside it is what this
334/// module exists to stop.
335#[must_use]
336pub fn selector_reaches_body_in_cell(lo: [i32; 3], hi: [i32; 3], cell: [i32; 3]) -> bool {
337    let half = PLAYER_WIDTH / 2.0;
338    // The extreme reach of a body standing anywhere in `cell`: horizontally every
339    // position of the cell, each carrying half a width past itself; vertically the
340    // feet on the cell floor and the head PLAYER_HEIGHT above them.
341    let body_lo = [
342        f64::from(cell[0]) - half,
343        f64::from(cell[1]),
344        f64::from(cell[2]) - half,
345    ];
346    let body_hi = [
347        f64::from(cell[0]) + 1.0 + half,
348        f64::from(cell[1]) + PLAYER_HEIGHT,
349        f64::from(cell[2]) + 1.0 + half,
350    ];
351    (0..3).all(|i| body_lo[i] <= f64::from(hi[i]) + 1.0 && body_hi[i] >= f64::from(lo[i]))
352}
353
354/// Ticks a jumping player spends off the ground, apex to landing included.
355pub const JUMP_AIRBORNE_TICKS: f64 = 12.0;
356
357/// Player walking speed on the flat, in blocks per second (not sprinting).
358pub const WALK_SPEED_BLOCKS_PER_SECOND: f64 = 4.317;
359
360/// Player sprinting speed on the flat, in blocks per second [cited — Minecraft
361/// Wiki, *Sprinting*: "around 5.612 meters/second, which is 30 percent faster
362/// than the normal walking speed of around 4.317 m/s"]. The reach a `darkness`
363/// grant owes the blind-reach proof is taken at this speed, because darkness,
364/// unlike blindness, does not forbid the sprint (spec-0085 §6.2).
365pub const SPRINT_SPEED_BLOCKS_PER_SECOND: f64 = 5.612;
366
367/// The wiki page [`SPRINT_SPEED_BLOCKS_PER_SECOND`] is read from.
368pub const SPRINT_SPEED_PAGE: &str = "Sprinting";
369
370/// Server ticks per second.
371pub const TICKS_PER_SECOND: f64 = 20.0;
372
373/// The fastest horizontal displacement a body makes in one tick without an
374/// item, in blocks: sprint-jumping with the jump held (spec-0086 §2.1).
375///
376/// Measured by `tools/spike-seamless-loop/` (the bot's own physics,
377/// prismarine-physics at the harness pin, on the pinned server): walking
378/// `0.2159`, sprinting `0.2806`, sprint-jumping `0.5878`. It is the bound a
379/// loop's horizontal slab is held to — a one-tick poll catches every crossing
380/// whose fastest tick is under the slab's thickness plus the body's reach.
381pub const POLL_HORIZONTAL_BLOCKS_PER_TICK: f64 = 0.5878;
382
383/// The speed a falling body approaches, in blocks per tick (spec-0086 §2.1):
384/// the fixed point `k·g / (1 − k)` of the fall law `v′ = k·(v + g)` with
385/// `k = 0.980`, `g = 0.080`, fitted over 87 consecutive per-tick pairs of one
386/// 184-block drop.
387///
388/// Measured by `tools/spike-seamless-loop/` with the same physics. It is the
389/// bound a loop's vertical slab is held to, the limit rather than the fastest
390/// tick one fall happened to reach (`3.333`): a one-cell slab caught 9 of 10
391/// drops and a three-cell slab 10 of 10.
392pub const POLL_FALL_BLOCKS_PER_TICK: f64 = 3.92;
393
394/// The fall distance in blocks below which vanilla deals no fall damage: damage
395/// is `ceil(distance − 3)` points, so a 3-block fall is free and a 4-block fall
396/// costs one.
397pub const FALL_DAMAGE_ONSET_BLOCKS: f64 = 3.0;
398
399/// Ticks a walking player spends crossing one block on the flat.
400///
401/// Derived rather than stored, because both operands are facts and nothing
402/// downstream decides a route on this number — it is the pacing denominator.
403#[must_use]
404pub fn walk_ticks_per_block() -> f64 {
405    TICKS_PER_SECOND / WALK_SPEED_BLOCKS_PER_SECOND
406}
407
408/// The largest fall an unarmoured player at full health survives, in blocks.
409///
410/// `ceil(d − 3) < 20` holds up to `d = 22`, which lands on one half-heart; 23
411/// blocks deals 20 and kills. Derived from [`FALL_DAMAGE_ONSET_BLOCKS`] and
412/// [`PLAYER_MAX_HEALTH`] so that moving either moves this, and it exists so the
413/// designed-drop policy beside it has a physical ceiling to be **tighter than**
414/// rather than a number chosen next to nothing.
415#[must_use]
416pub fn unarmoured_survivable_fall_blocks() -> f64 {
417    (FALL_DAMAGE_ONSET_BLOCKS + PLAYER_MAX_HEALTH - 1.0).floor()
418}
419
420/// Horizontal cells a standing body needs to pass: `ceil(0.6)`.
421#[must_use]
422pub fn passable_width_cells() -> u32 {
423    PLAYER_WIDTH.ceil() as u32
424}
425
426/// Vertical cells a standing body needs to pass: `ceil(1.8)`.
427///
428/// This is a **player** metric and the spec's building half listed it, which is
429/// the correction worth naming: the width and clearance at which a body can pass
430/// at all are functions of the collision box, so no walk can change them and
431/// `calibrated` would mean nothing on them. What the gym calibrates is the
432/// *designed* minimum — a way class's `min_width` and `min_clearance` — which is
433/// a comfort judgement and can never be chosen below this floor —
434/// [`Metrics::self_check`] is what holds it there, over every way class the
435/// table defines rather than over the two entries that used to stand alone.
436#[must_use]
437pub fn passable_clearance_cells() -> u32 {
438    PLAYER_HEIGHT.ceil() as u32
439}
440
441// ---------------------------------------------------------------------------
442// A body has a width — the one answer to "does this body meet this volume"
443// ---------------------------------------------------------------------------
444
445/// A body's standing collision box: `width × height × width`, centred
446/// horizontally on the body's position and rising from its feet.
447///
448/// It lives here for the reason [`step_allowed`] does. Three consumers ask the
449/// same geometric question of the same object class — the routing model asks
450/// which cells a walker may occupy beside a killing volume, the posted-place
451/// proof asks whether a body seated at a declared cell is inside one, and the
452/// emitter formats the runtime selector that decides both at play time — and a
453/// question with three answers is a question three gates can disagree about.
454/// The disagreement was not hypothetical: every proof reasoned in whole cells
455/// while the runtime selected on hitbox intersection, so the engine routed a
456/// player along a cell whose occupant the volume kills, chose that same cell as
457/// the "safe" lip a stake stands on, and reported both green.
458#[derive(Debug, Clone, Copy, PartialEq)]
459pub struct Body {
460    /// Collision-box width and depth, in blocks.
461    pub width: f64,
462    /// Collision-box height, in blocks.
463    pub height: f64,
464}
465
466impl Body {
467    /// The player's own body, from the constants above rather than from a
468    /// second pair of literals.
469    pub const PLAYER: Body = Body {
470        width: PLAYER_WIDTH,
471        height: PLAYER_HEIGHT,
472    };
473
474    /// A body of the given standing hitbox.
475    #[must_use]
476    pub fn new(width: f64, height: f64) -> Body {
477        Body { width, height }
478    }
479
480    /// Half the width — the reach of the box either side of the body's own
481    /// position.
482    #[must_use]
483    pub fn half_width(&self) -> f64 {
484        self.width / 2.0
485    }
486}
487
488/// The world-space AABB vanilla builds from a box selector over the inclusive
489/// cell box `lo..=hi`.
490///
491/// **This is a fact about the game, and it is what the emitter writes.** A box
492/// selector is spelled `x=lo,dx=hi-lo,…` (`emit::box_selector_args`), and
493/// vanilla's `EntitySelectorParser` turns `x`/`dx` into the half-open span
494/// `[x, x+dx+1]` — which is why `dx=0` selects the whole of one block rather
495/// than a plane. So the inclusive cell box `lo..=hi` is the world volume
496/// `[lo, hi+1]`, and an entity is selected when its **hitbox intersects** it,
497/// never when the cell it stands in is one of the box's cells.
498///
499/// Stated as a [`Provenance::VanillaRule`] would be: this repository has not
500/// measured the parser, but it has measured the consequence — bodies whose feet
501/// cell lies outside a declared volume are killed by it, at the two faces this
502/// arithmetic predicts.
503#[must_use]
504pub fn selection_aabb(lo: [i32; 3], hi: [i32; 3]) -> ([f64; 3], [f64; 3]) {
505    (
506        [lo[0] as f64, lo[1] as f64, lo[2] as f64],
507        [hi[0] as f64 + 1.0, hi[1] as f64 + 1.0, hi[2] as f64 + 1.0],
508    )
509}
510
511/// Does a body standing with its feet at `feet` meet the volume `lo..=hi`?
512///
513/// `feet` is the position vanilla keeps for an entity: the horizontal centre of
514/// its collision box, at the box's base. So the body occupies
515/// `[x ± w/2] × [y, y + h] × [z ± w/2]`, and the answer is whether that
516/// intersects [`selection_aabb`]. The comparison is strict on both sides, as
517/// vanilla's `AABB::intersects` is: a body whose face exactly touches the
518/// volume's face is not inside it.
519///
520/// This is the **exact** reading, for a body whose position is known — a summon
521/// writes literal coordinates, and `compiler::nav::cell_center` is the one place
522/// that says which coordinates a body seated on a cell gets. For a body that may
523/// stand anywhere within a cell, ask [`cell_can_meet_volume`] instead.
524#[must_use]
525pub fn body_meets_volume(feet: [f64; 3], body: Body, lo: [i32; 3], hi: [i32; 3]) -> bool {
526    let (vlo, vhi) = selection_aabb(lo, hi);
527    let half = body.half_width();
528    let blo = [feet[0] - half, feet[1], feet[2] - half];
529    let bhi = [feet[0] + half, feet[1] + body.height, feet[2] + half];
530    (0..3).all(|i| blo[i] < vhi[i] && bhi[i] > vlo[i])
531}
532
533/// **The inclusive cell box a body of these dimensions must keep out of** — the
534/// cells from which its hitbox can reach the volume `lo..=hi`.
535///
536/// A walker's cell does not fix its position: a body whose feet block is `c`
537/// stands anywhere in `[c, c+1)` horizontally, so its box spans
538/// `[c - w/2, c + 1 + w/2)`. The cells that can meet the volume are therefore
539/// the volume's own, widened by however many cells half a width reaches into —
540/// one, for every body in the engine's dims table, and the derivation rather
541/// than the constant is what is written here so a wider body widens it.
542///
543/// Vertically the body's feet sit at the cell's own floor and it rises `height`
544/// blocks, so the box reaches `ceil(height)` cells down from the volume's floor
545/// and not at all above its ceiling: standing **on top** of a volume is safe,
546/// standing under one with your head in it is not.
547///
548/// The direction is deliberate and is the opposite of
549/// `compiler::reach::ReachCompletion::certainly_completes_from`, which asks the
550/// same geometry about a volume that GRANTS something and so demands the certain
551/// case. Here the volume kills, so the refusal takes the possible case: a
552/// generous rule is safe for a reward and unsound for a hazard.
553#[must_use]
554pub fn keep_out_box(body: Body, lo: [i32; 3], hi: [i32; 3]) -> ([i32; 3], [i32; 3]) {
555    let half = body.half_width();
556    let mut out_lo = [0i32; 3];
557    let mut out_hi = [0i32; 3];
558    for i in [0usize, 2] {
559        // `c + 1 + half > lo` for some c, and `c - half < hi + 1`.
560        out_lo[i] = (lo[i] as f64 - 1.0 - half).floor() as i32 + 1;
561        out_hi[i] = (hi[i] as f64 + 1.0 + half).ceil() as i32 - 1;
562    }
563    // `c + height > lo_y` (feet at the cell floor), and `c <= hi_y` — a body
564    // whose feet are at `hi_y + 1` starts exactly where the volume ends.
565    out_lo[1] = (lo[1] as f64 - body.height).floor() as i32 + 1;
566    out_hi[1] = hi[1];
567    (out_lo, out_hi)
568}
569
570/// Can a body whose feet block is `cell` meet the volume `lo..=hi` from **some**
571/// position inside that cell? The refusing reading — see [`keep_out_box`].
572#[must_use]
573pub fn cell_can_meet_volume(cell: [i32; 3], body: Body, lo: [i32; 3], hi: [i32; 3]) -> bool {
574    let (klo, khi) = keep_out_box(body, lo, hi);
575    (0..3).all(|i| klo[i] <= cell[i] && cell[i] <= khi[i])
576}
577
578// ---------------------------------------------------------------------------
579// Entry shapes
580// ---------------------------------------------------------------------------
581
582/// Where a number came from. The four are not interchangeable — see the module
583/// docs.
584#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
585#[serde(rename_all = "kebab-case")]
586pub enum Provenance {
587    /// The number **is** a constant this module defines and the rest of the
588    /// workspace imports. One definition, so nothing to drift from.
589    EngineConstant,
590    /// A stated rule of pinned Minecraft Java 1.21.11 that no engine constant
591    /// held before this table, and that this repository has **not** measured on
592    /// a running server. The note names the rule so the claim is checkable.
593    VanillaRule,
594    /// Computed from other entries; the note carries the arithmetic.
595    Derived,
596    /// A seed for the metrics gym's calibration walk. Chosen, not established.
597    Provisional,
598}
599
600/// A named opening in the standard seam set, in cells.
601#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
602pub struct Opening {
603    /// Clear width, in cells.
604    pub width: u32,
605    /// Clear height, in cells.
606    pub height: u32,
607}
608
609/// A named stair-pitch standard: a rise:run pattern together with the vanilla
610/// blocks that realize it, and the per-step rise the realization actually
611/// presents to a walking body.
612#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
613pub struct Pitch {
614    /// Blocks of rise per `run` blocks of horizontal travel.
615    pub rise: u32,
616    /// Blocks of horizontal travel per `rise` blocks of rise.
617    pub run: u32,
618    /// The rise in sixteenths that one realized tread presents. A stair block
619    /// offers its lower half first, so a 1:1 stair run steps 8/16 twice per
620    /// block of rise rather than 16/16 once; a slab ramp steps 8/16 as well.
621    /// This is the number [`Metrics::self_check`] holds under the walk-up
622    /// budget, which is what makes "standard pitch" mean *walked*, not merely
623    /// *legal*.
624    pub step_16: i64,
625    /// The vanilla realization, named so the derivation above is checkable
626    /// against blocks rather than asserted.
627    pub realization: &'static str,
628}
629
630/// A rung of the size-class ladder — the vocabulary a layout-graph node declares
631/// and a site-plan box is judged against.
632#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
633pub struct SizeClass {
634    /// Smallest interior footprint, `[x, z]` in cells, inclusive.
635    pub min_footprint: [u32; 2],
636    /// Largest interior footprint, `[x, z]` in cells, inclusive.
637    pub max_footprint: [u32; 2],
638    /// Least interior clearance, in cells.
639    pub min_clearance: u32,
640    /// Nominal blocks of route a body walks crossing a place of this class —
641    /// the per-leg length the pacing projection sums over a critical path
642    /// (spec-0049 §3.3).
643    ///
644    /// The code that reads it is not named here on purpose: a `DW` number in a
645    /// source comment is a code as far as `tools/ci/check-dw-codes.py` is
646    /// concerned, and one whose check lands two rounds from now has no catalog
647    /// row to match, so naming it early reds the docs job on a rule nothing has
648    /// written yet.
649    pub nominal_traverse_blocks: u32,
650}
651
652/// A **way class** — the vocabulary a layout-graph node declares for a place
653/// whose footprint is bounded in one axis and free in the other: a road, a
654/// causeway, a corridor, a duct (spec-0053 §3).
655///
656/// # Why it is a second kind of classification and not a rung
657///
658/// The size-class ladder classifies a place by both horizontal extents at once,
659/// and that is what it is for. A route has no second extent to classify: a cut
660/// ledge one body wide climbing a whole seaward face is 4 by 90, and for any
661/// rung to admit it that rung would have to span 4..90 on an axis — a class in
662/// which an alcove and an expanse are the same thing has stopped classifying.
663/// The failure is by KIND, not by margin, which is why no calibration of the
664/// ladder reaches it.
665///
666/// # What it bounds, and what it deliberately does not
667///
668/// A way class bounds the **cross-section** — the axis a body feels walking it
669/// — and says nothing whatever about the run. A route's length is per-campaign
670/// geometry, not a standard: a village lane, a canyon rim trail, a ship's
671/// gangway and a mine gallery are the same class of thing at four wildly
672/// different lengths, and a `max_length` here would be this month's map wearing
673/// a standard's clothes. The run is *measured*, into pacing, and is never
674/// compared against anything (spec-0053 §7).
675///
676/// The elongation demand a way-classed box must satisfy is therefore
677/// **structural rather than a constant**: the run must exceed
678/// [`Self::max_width`], which is exactly what a room cannot supply. A square box
679/// can never qualify, because its "run" equals its width and one number cannot
680/// both be `<= max_width` and exceed it. That is what makes "declare it a way to
681/// escape the ladder" refused by the object's own shape rather than by a rule
682/// the author could satisfy by choosing differently.
683#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
684pub struct WayClass {
685    /// Narrowest cross-section, in cells, inclusive. Never below the physical
686    /// passable width — [`Metrics::self_check`] holds it there.
687    pub min_width: u32,
688    /// Widest cross-section, in cells, inclusive. Doubles as the elongation
689    /// floor: a way-classed box's run must **exceed** this.
690    pub max_width: u32,
691    /// Least interior clearance, in cells.
692    pub min_clearance: u32,
693}
694
695/// The kit grid: the quantum box extents are multiples of, and the datum
696/// convention that fixes what a declared `y` means.
697#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
698pub struct Grid {
699    /// The footprint quantum `q`, in blocks.
700    pub quantum: u32,
701    /// The axes extents are quantized on. Vertical extent is not quantized:
702    /// storey heights are their own entries and a box's height follows them.
703    pub axes: [&'static str; 2],
704    /// What a box's declared datum `y` names. `floor-surface`: the walk plane is
705    /// at `y`, and whatever stands in the box later puts its own floor there.
706    pub datum: &'static str,
707}
708
709/// One entry's value. `untagged`, so the export reads as the number or object it
710/// is rather than as a wrapper a consumer has to unpick.
711#[derive(Debug, Clone, PartialEq, Serialize)]
712#[serde(untagged)]
713pub enum MetricValue {
714    /// A count of cells, blocks or ticks.
715    Count(u32),
716    /// A measurement that is not a whole number of anything.
717    Number(f64),
718    /// A yes/no fact.
719    Flag(bool),
720    /// A named seam opening.
721    Opening(Opening),
722    /// A named stair pitch.
723    Pitch(Pitch),
724    /// A rung of the size-class ladder.
725    SizeClass(SizeClass),
726    /// A way class — a route's cross-section.
727    WayClass(WayClass),
728    /// The kit grid.
729    Grid(Grid),
730}
731
732/// One player metric: a fact of the pinned game.
733#[derive(Debug, Clone, PartialEq, Serialize)]
734pub struct PlayerEntry {
735    /// The value.
736    pub value: MetricValue,
737    /// What the value counts (`blocks`, `sixteenths`, `ticks`, …), or `none`.
738    pub unit: &'static str,
739    /// Where the number came from.
740    pub provenance: Provenance,
741    /// One sentence: the constant this **is**, the vanilla rule it states, or
742    /// the arithmetic it was derived by.
743    pub note: &'static str,
744}
745
746/// One building metric: a standard this project fixes.
747///
748/// The value is deliberately not a public field. [`BuildingEntry::value`] is the
749/// only way to read it and it takes `&mut `[`Reads`], so nothing can rest a
750/// verdict on an uncalibrated standard without saying that it did.
751#[derive(Debug, Clone, PartialEq, Serialize)]
752pub struct BuildingEntry {
753    /// The entry's own key, carried so a read records itself without the caller
754    /// having to repeat the name it looked up under.
755    key: &'static str,
756    value: MetricValue,
757    /// What the value counts, or `none`.
758    pub unit: &'static str,
759    /// Where the number came from. Orthogonal to `calibrated`: a pitch's
760    /// geometry can be derived from vanilla blocks while the judgement that it
761    /// is *the* standard is still unwalked.
762    pub provenance: Provenance,
763    /// Whether the metrics gym's walk has ruled on this value. `false` on every
764    /// entry at this version.
765    pub calibrated: bool,
766    /// One sentence: what the number is for and what the gym is being asked to
767    /// decide about it.
768    pub note: &'static str,
769}
770
771impl BuildingEntry {
772    /// Read the value, recording the read.
773    ///
774    /// The `&mut `[`Reads`] is the whole mechanism: [`Metrics::notice`] reports
775    /// exactly the uncalibrated entries a run actually consumed, so `DW0813`
776    /// cannot be forgotten by a check that reads one and can never fire over a
777    /// standard nothing looked at.
778    #[must_use]
779    pub fn value(&self, reads: &mut Reads) -> &MetricValue {
780        reads.record(self);
781        &self.value
782    }
783
784    /// The value with no read recorded — for rendering the table, never for
785    /// deciding anything.
786    ///
787    /// Reachable only inside this crate, and used at exactly one site: the
788    /// export, which reports the table rather than resting a verdict on it. A
789    /// serialization is not a verdict, and counting it as one would put every
790    /// entry in every `DW0813` line and make the code mean nothing.
791    pub(crate) fn value_for_display(&self) -> &MetricValue {
792        &self.value
793    }
794
795    /// The entry's key.
796    #[must_use]
797    pub fn key(&self) -> &'static str {
798        self.key
799    }
800}
801
802/// The building metrics a run's verdicts have read.
803///
804/// Deterministic (ADR-0006): a `BTreeSet` of `&'static str`, so the order a
805/// `DW0813` line names them in is the table's own order and not the order the
806/// checks happened to run in.
807#[derive(Debug, Clone, Default, PartialEq, Eq)]
808pub struct Reads {
809    read: BTreeSet<&'static str>,
810    provisional: BTreeSet<&'static str>,
811}
812
813impl Reads {
814    /// A ledger nothing has read through yet.
815    #[must_use]
816    pub fn new() -> Self {
817        Self::default()
818    }
819
820    fn record(&mut self, entry: &BuildingEntry) {
821        self.read.insert(entry.key);
822        if !entry.calibrated {
823            self.provisional.insert(entry.key);
824        }
825    }
826
827    /// How many building metrics this run's verdicts read, and how many of those
828    /// the gym has not walked.
829    #[must_use]
830    pub fn binding(&self) -> ReadBinding {
831        ReadBinding {
832            read: self.read.len(),
833            provisional: self.provisional.len(),
834        }
835    }
836
837    /// The uncalibrated entries read, in table order.
838    #[must_use]
839    pub fn provisional(&self) -> Vec<&'static str> {
840        self.provisional.iter().copied().collect()
841    }
842
843    /// Every entry read, in table order.
844    ///
845    /// The metrics gym's coverage numerator: an entry the gym's construction
846    /// never read is an entry no bay was built from, so a walk of the gym cannot
847    /// rule on it. Taking it from the same ledger the reads are recorded in is
848    /// what stops the gym's own coverage claim being a list somebody maintains
849    /// beside the generator.
850    #[must_use]
851    pub fn read(&self) -> BTreeSet<&'static str> {
852        self.read.clone()
853    }
854}
855
856/// What a run's building-metric reads bound to.
857#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
858pub struct ReadBinding {
859    /// Building metrics read.
860    pub read: usize,
861    /// Of those, entries whose `calibrated` is false.
862    pub provisional: usize,
863}
864
865/// A name a document wrote that this table does not define — `DW0812`'s payload.
866///
867/// It carries the defined set as well as the bad name, because the author's next
868/// action is choosing a real one and a refusal that only says *no* sends them to
869/// read the compiler.
870#[derive(Debug, Clone, PartialEq, Eq)]
871pub struct UnknownMetric {
872    /// The kind of entry the document was naming (`size class`, `opening`, …).
873    pub kind: &'static str,
874    /// The same kind, plural — carried rather than derived, because three of the
875    /// six nouns end in a sibilant and `{kind}s` is wrong for them.
876    pub kind_plural: &'static str,
877    /// The name it wrote.
878    pub named: String,
879    /// Every name this table does define of that kind, in table order.
880    pub defined: Vec<&'static str>,
881}
882
883impl UnknownMetric {
884    /// The `DW0812` refusal, located by the caller.
885    #[must_use]
886    pub fn diagnostic(&self, stage: &str, path: &str) -> Diagnostic {
887        let defined = if self.defined.is_empty() {
888            "nothing".to_string()
889        } else {
890            self.defined.join(", ")
891        };
892        Diagnostic::error(
893            DW_METRIC_UNKNOWN,
894            stage,
895            path,
896            format!(
897                "the metrics table defines no {kind} called `{named}`. The table is \
898                 the single authority for this vocabulary, so a name it does not \
899                 define cannot compile and no check downstream has to cope with one. \
900                 Defined {plural}: {defined}. Run `delvec metrics` for the whole \
901                 table, including what each entry is for.",
902                kind = self.kind,
903                plural = self.kind_plural,
904                named = self.named,
905            ),
906        )
907    }
908}
909
910// ---------------------------------------------------------------------------
911// The table
912// ---------------------------------------------------------------------------
913
914/// The metrics standard, as exported and as read.
915#[derive(Debug, Clone, PartialEq, Serialize)]
916pub struct Metrics {
917    /// The table's revision.
918    pub metrics_version: u32,
919    /// The Minecraft version the player half states facts about.
920    pub mc_version: &'static str,
921    /// Facts of the pinned game, in key order.
922    pub player: BTreeMap<&'static str, PlayerEntry>,
923    /// Standards this project fixes, in key order.
924    pub building: BTreeMap<&'static str, BuildingEntry>,
925}
926
927/// Kinds of building entry a document can name, for [`Metrics::resolve`].
928#[derive(Debug, Clone, Copy, PartialEq, Eq)]
929pub enum MetricKind {
930    /// A seam opening (`opening.<name>`).
931    Opening,
932    /// A stair pitch (`pitch.<name>`).
933    Pitch,
934    /// A rung of the size-class ladder (`size-class.<name>`).
935    SizeClass,
936    /// A way class (`way-class.<name>`) — the second kind of place
937    /// classification (spec-0053 §3). A [`MetricKind`] rather than a lookup of
938    /// its own for the reason [`Metrics::resolve`] gives: this is a name a
939    /// DOCUMENT writes, so it goes through the one path from a name to an entry
940    /// and a name the table does not define is `DW0812` here exactly as it is
941    /// for a size class.
942    WayClass,
943    /// A storey height (`storey.<name>`).
944    Storey,
945    /// A pacing coefficient (`pacing.<name>`) — blocks of route per minute of
946    /// play. Named like the rest of the vocabulary rather than reached by a
947    /// second lookup, because [`Metrics::resolve`] is the ONE path from a name
948    /// to an entry and a coefficient read any other way would be a second
949    /// authority (spec-0049 §2.2). No campaign document names one; the pacing
950    /// projection does.
951    Pacing,
952}
953
954impl MetricKind {
955    /// The key prefix entries of this kind carry.
956    #[must_use]
957    pub fn prefix(self) -> &'static str {
958        match self {
959            MetricKind::Opening => "opening.",
960            MetricKind::Pitch => "pitch.",
961            MetricKind::SizeClass => "size-class.",
962            MetricKind::WayClass => "way-class.",
963            MetricKind::Storey => "storey.",
964            MetricKind::Pacing => "pacing.",
965        }
966    }
967
968    /// What the kind is called in a refusal, **plural**.
969    ///
970    /// A fact about the kind rather than an `s` appended where a message needed
971    /// one: three of the six nouns end in a sibilant, so `{noun}s` reads
972    /// `size classs`, `stair pitchs` and `way classs`. Written out here, a kind
973    /// added later cannot inherit that by default — it has to answer.
974    #[must_use]
975    pub fn plural(self) -> &'static str {
976        match self {
977            MetricKind::Opening => "seam openings",
978            MetricKind::Pitch => "stair pitches",
979            MetricKind::SizeClass => "size classes",
980            MetricKind::WayClass => "way classes",
981            MetricKind::Storey => "storey heights",
982            MetricKind::Pacing => "pacing coefficients",
983        }
984    }
985
986    /// What the kind is called in a refusal.
987    #[must_use]
988    pub fn noun(self) -> &'static str {
989        match self {
990            MetricKind::Opening => "seam opening",
991            MetricKind::Pitch => "stair pitch",
992            MetricKind::SizeClass => "size class",
993            MetricKind::WayClass => "way class",
994            MetricKind::Storey => "storey height",
995            MetricKind::Pacing => "pacing coefficient",
996        }
997    }
998}
999
1000fn player(
1001    value: MetricValue,
1002    unit: &'static str,
1003    provenance: Provenance,
1004    note: &'static str,
1005) -> PlayerEntry {
1006    PlayerEntry {
1007        value,
1008        unit,
1009        provenance,
1010        note,
1011    }
1012}
1013
1014fn building(
1015    key: &'static str,
1016    value: MetricValue,
1017    unit: &'static str,
1018    provenance: Provenance,
1019    note: &'static str,
1020) -> (&'static str, BuildingEntry) {
1021    (
1022        key,
1023        BuildingEntry {
1024            key,
1025            value,
1026            unit,
1027            provenance,
1028            // Every entry lands uncalibrated. The gym walk flips it, one entry
1029            // at a time, and that walk is the only thing that may.
1030            calibrated: false,
1031            note,
1032        },
1033    )
1034}
1035
1036impl Metrics {
1037    /// The table.
1038    ///
1039    /// Rebuilt per call rather than held in a `static`, because the values are a
1040    /// handful of maps and a `static` would need interior mutability the moment
1041    /// the gym's walk starts flipping `calibrated`. Deterministic by
1042    /// construction: `BTreeMap`, no clock, no environment.
1043    #[must_use]
1044    pub fn table() -> Self {
1045        let player_entries: Vec<(&'static str, PlayerEntry)> = vec![
1046            (
1047                "body.width",
1048                player(
1049                    MetricValue::Number(PLAYER_WIDTH),
1050                    "blocks",
1051                    Provenance::EngineConstant,
1052                    "The standing collision box is 0.6 wide; `dsl::metrics::PLAYER_WIDTH` \
1053                     is the definition `compiler::nav`'s entity dims table and \
1054                     `compiler::crosshair` both import.",
1055                ),
1056            ),
1057            (
1058                "body.height",
1059                player(
1060                    MetricValue::Number(PLAYER_HEIGHT),
1061                    "blocks",
1062                    Provenance::EngineConstant,
1063                    "The standing collision box is 1.8 tall; the same definition the \
1064                     entity dims table reads for `minecraft:player`.",
1065                ),
1066            ),
1067            (
1068                "body.crouched-height",
1069                player(
1070                    MetricValue::Number(PLAYER_CROUCHED_HEIGHT),
1071                    "blocks",
1072                    Provenance::VanillaRule,
1073                    "A sneaking player's collision box is 1.5 tall, which is what lets a \
1074                     body pass under a cell a standing one cannot. No engine constant \
1075                     held this before the table, and nothing in this workspace has \
1076                     measured it on a running server.",
1077                ),
1078            ),
1079            (
1080                "body.eye-height",
1081                player(
1082                    MetricValue::Number(PLAYER_EYE_HEIGHT),
1083                    "blocks",
1084                    Provenance::EngineConstant,
1085                    "The eye sits 1.62 above the floor of the cell the body stands in. \
1086                     This is the definition the render plan, the viewer page and the \
1087                     creator overlay all import, having each declared their own before.",
1088                ),
1089            ),
1090            (
1091                "body.max-health",
1092                player(
1093                    MetricValue::Number(PLAYER_MAX_HEALTH),
1094                    "half-hearts",
1095                    Provenance::EngineConstant,
1096                    "Twenty points of health, the definition `compiler::combat` imports \
1097                     for its winnability arithmetic.",
1098                ),
1099            ),
1100            (
1101                "step.walk-up",
1102                player(
1103                    MetricValue::Count(MAX_AUTO_STEP_16 as u32),
1104                    "sixteenths",
1105                    Provenance::EngineConstant,
1106                    "The largest rise a walker crosses without jumping: vanilla's player \
1107                     `maxUpStep` is 0.6 blocks and 9/16 is the largest sixteenth under \
1108                     it. `compiler::nav` imports this as its step rule's walk-up budget.",
1109                ),
1110            ),
1111            (
1112                "step.jump-rise",
1113                player(
1114                    MetricValue::Count(MAX_JUMP_RISE_16 as u32),
1115                    "sixteenths",
1116                    Provenance::EngineConstant,
1117                    "The largest rise a walker reaches by jumping: the apex is ≈1.2522 \
1118                     blocks, so 20/16 is reachable and 21/16 is not. `compiler::nav` \
1119                     imports this, and it is why a slab-to-full-block step-up of 1.5 is \
1120                     the impossible move it is.",
1121                ),
1122            ),
1123            (
1124                "step.block",
1125                player(
1126                    MetricValue::Count(FULL_16 as u32),
1127                    "sixteenths",
1128                    Provenance::EngineConstant,
1129                    "A full block in the sixteenths the step rule is denominated in, so \
1130                     that no comparison in the navigation model is a float.",
1131                ),
1132            ),
1133            (
1134                "jump.airborne",
1135                player(
1136                    MetricValue::Number(JUMP_AIRBORNE_TICKS),
1137                    "ticks",
1138                    Provenance::VanillaRule,
1139                    "A jump is airborne about twelve ticks, apex to landing. This was \
1140                     prose in the navigation model's elevation-weight derivation and \
1141                     nothing could read it; the table is where it becomes data.",
1142                ),
1143            ),
1144            (
1145                "walk.speed",
1146                player(
1147                    MetricValue::Number(WALK_SPEED_BLOCKS_PER_SECOND),
1148                    "blocks/second",
1149                    Provenance::VanillaRule,
1150                    "A walking player covers 4.317 blocks a second on the flat; \
1151                     sprinting is faster and is not the pacing basis, because a route \
1152                     nobody has learnt is walked.",
1153                ),
1154            ),
1155            (
1156                "sprint.speed",
1157                player(
1158                    MetricValue::Number(SPRINT_SPEED_BLOCKS_PER_SECOND),
1159                    "blocks/second",
1160                    Provenance::VanillaRule,
1161                    "A sprinting player covers 5.612 blocks a second on the flat (the \
1162                     wiki's Sprinting page). Not a pacing basis; it is how far a body can \
1163                     carry itself under a darkness grant, which leaves the sprint, in the \
1164                     blind-reach proof.",
1165                ),
1166            ),
1167            (
1168                "walk.ticks-per-block",
1169                player(
1170                    MetricValue::Number(walk_ticks_per_block()),
1171                    "ticks",
1172                    Provenance::Derived,
1173                    "Twenty ticks a second over 4.317 blocks a second. Against the \
1174                     twelve airborne ticks of a jump this is what makes a block of \
1175                     climb cost about two and a half blocks of walking, which is where \
1176                     the navigation model's elevation weight of two comes from.",
1177                ),
1178            ),
1179            (
1180                "fluid.passable",
1181                player(
1182                    MetricValue::Flag(false),
1183                    "none",
1184                    Provenance::VanillaRule,
1185                    "Water and lava are impassable to every proof in this engine and are \
1186                     never floor either: a body cannot stand on a fluid surface, so the \
1187                     two sets are disjoint and both gate standability. Marked as a rule \
1188                     rather than an engine constant deliberately — the navigation model \
1189                     encodes this as the SHAPE of its occupancy sets and not as a shared \
1190                     `const`, so there is no single definition for this row to be, and \
1191                     claiming otherwise would be the overclaim the provenance field \
1192                     exists to prevent.",
1193                ),
1194            ),
1195            (
1196                "fall.damage-onset",
1197                player(
1198                    MetricValue::Number(FALL_DAMAGE_ONSET_BLOCKS),
1199                    "blocks",
1200                    Provenance::VanillaRule,
1201                    "Fall damage is `ceil(distance − 3)` points, so a three-block fall is \
1202                     free and a four-block fall costs one. Stated from the vanilla \
1203                     damage rule; this repository has not measured it on a running \
1204                     server.",
1205                ),
1206            ),
1207            (
1208                "fall.unarmoured-survivable",
1209                player(
1210                    MetricValue::Number(unarmoured_survivable_fall_blocks()),
1211                    "blocks",
1212                    Provenance::Derived,
1213                    "`ceil(distance − 3) < 20` holds up to 22 blocks, which lands a \
1214                     full-health unarmoured body on one half-heart; 23 deals twenty and \
1215                     kills. The survivable ceiling is a function of health and armour, \
1216                     and this is its unarmoured, full-health case — the physical bound \
1217                     the designed-drop policy is deliberately tighter than.",
1218                ),
1219            ),
1220            (
1221                "passable.width",
1222                player(
1223                    MetricValue::Count(passable_width_cells()),
1224                    "cells",
1225                    Provenance::Derived,
1226                    "`ceil(0.6)`: one cell is the narrowest a standing body fits \
1227                     through. This is a fact, not a standard — no walk can change it, \
1228                     and it is the floor the designed corridor minimum may never be \
1229                     chosen below.",
1230                ),
1231            ),
1232            (
1233                "passable.clearance",
1234                player(
1235                    MetricValue::Count(passable_clearance_cells()),
1236                    "cells",
1237                    Provenance::Derived,
1238                    "`ceil(1.8)`: two cells is the lowest a standing body passes under. \
1239                     Like the width beside it this is a fact rather than a standard, \
1240                     which is why neither carries a calibration flag.",
1241                ),
1242            ),
1243        ];
1244
1245        let building_entries: Vec<(&'static str, BuildingEntry)> = vec![
1246            building(
1247                "grid",
1248                MetricValue::Grid(Grid {
1249                    quantum: 4,
1250                    axes: ["x", "z"],
1251                    datum: "floor-surface",
1252                }),
1253                "blocks",
1254                Provenance::Provisional,
1255                "The footprint quantum every site-plan box's horizontal extents are \
1256                 multiples of, and the datum convention: a box's floor SURFACE is at \
1257                 its declared y, and whatever stands in the box later puts its walk \
1258                 plane there. Four is a seed and nothing in the existing piece library \
1259                 argues for it — the cave tileset is odd on every axis and the keep \
1260                 tileset is even but not quartered — so what the gym is being asked is \
1261                 whether a quantum this fine buys anything a coarser one would not.",
1262            ),
1263            building(
1264                "way-class.corridor",
1265                MetricValue::WayClass(WayClass {
1266                    min_width: 2,
1267                    max_width: 4,
1268                    min_clearance: 3,
1269                }),
1270                "cells",
1271                Provenance::Provisional,
1272                "The narrow way: a passage, a duct, a gallery cut through rock. Its \
1273                 `min_width` and `min_clearance` ARE the two numbers this table used to \
1274                 publish as `corridor.min-width` and `corridor.min-clearance` — one \
1275                 cell is passable and reads as a crawlspace, two lets two bodies pass, \
1276                 and two blocks of clearance puts the ceiling on the walker's head — and \
1277                 they are fields here rather than entries of their own so that there is \
1278                 one authority for the narrow way rather than a class beside two loose \
1279                 numbers nothing could spell. The gym walks widths one, two and three \
1280                 and clearances two, three and four. It is also asked a question that \
1281                 could not be posed while these numbers were unreachable: the kit \
1282                 quantum beside them is 4 and every box extent is a multiple of it, so \
1283                 the narrowest way any plan can currently DRAW is four cells, and the \
1284                 walk decides whether the floor moves up or the quantum moves down.",
1285            ),
1286            building(
1287                "way-class.road",
1288                MetricValue::WayClass(WayClass {
1289                    min_width: 4,
1290                    max_width: 16,
1291                    min_clearance: 6,
1292                }),
1293                "cells",
1294                Provenance::Provisional,
1295                "The broad way: a village lane, a causeway, a quay, a ledge cut across a \
1296                 cliff face. Wide enough that a party walks it abreast and something can \
1297                 come the other way, which is the difference from the corridor beside it \
1298                 and is what the walk is being asked to place. The clearance seed is \
1299                 higher than the corridor's because a way this wide reads as roofless \
1300                 even when it is not, and a low ceiling over a broad floor is the one \
1301                 combination that reads as a mistake.",
1302            ),
1303            building(
1304                "opening.door",
1305                MetricValue::Opening(Opening {
1306                    width: 1,
1307                    height: 2,
1308                }),
1309                "cells",
1310                Provenance::Provisional,
1311                "The narrow seam: one body at a time, the size of a vanilla door. Seeded \
1312                 at the smallest opening a standing body passes, so the gym is deciding \
1313                 whether the tightest legal seam is one anybody wants to walk.",
1314            ),
1315            building(
1316                "opening.arch",
1317                MetricValue::Opening(Opening {
1318                    width: 2,
1319                    height: 3,
1320                }),
1321                "cells",
1322                Provenance::Provisional,
1323                "The ordinary seam between two interior places: two abreast, headroom \
1324                 over both.",
1325            ),
1326            building(
1327                "opening.passage",
1328                MetricValue::Opening(Opening {
1329                    width: 3,
1330                    height: 3,
1331                }),
1332                "cells",
1333                Provenance::Derived,
1334                "The three-by-three doorway the existing jigsaw socket conventions \
1335                 already standardize on — `cave:socket` and `tk:socket` are both this \
1336                 opening, so the prefab library has been built against it for as long \
1337                 as it has existed. Derived from that convention rather than chosen \
1338                 here, and still uncalibrated: what the gym decides is whether the \
1339                 convention is right, not what it is.",
1340            ),
1341            building(
1342                "opening.gateway",
1343                MetricValue::Opening(Opening {
1344                    width: 5,
1345                    height: 5,
1346                }),
1347                "cells",
1348                Provenance::Provisional,
1349                "The broad seam a thing of scenery scale passes: a cart, a barge, a \
1350                 processional. Seeded wide enough to read as an event from inside the \
1351                 place it opens onto, which is the judgement the walk is for.",
1352            ),
1353            building(
1354                "pitch.stair",
1355                MetricValue::Pitch(Pitch {
1356                    rise: 1,
1357                    run: 1,
1358                    step_16: 8,
1359                    realization: "minecraft:*_stairs",
1360                }),
1361                "none",
1362                Provenance::Derived,
1363                "One block of rise per block of run, realized in stair blocks. The \
1364                 geometry is vanilla's: a stair offers its lower half first, so the \
1365                 body walks two eight-sixteenth steps per block of rise and never \
1366                 jumps. What is uncalibrated is the comfort judgement — whether a climb \
1367                 this steep is one a player wants to make repeatedly.",
1368            ),
1369            building(
1370                "pitch.ramp",
1371                MetricValue::Pitch(Pitch {
1372                    rise: 1,
1373                    run: 2,
1374                    step_16: 8,
1375                    realization: "minecraft:*_slab + full block",
1376                }),
1377                "none",
1378                Provenance::Derived,
1379                "One block of rise per two of run, realized as a bottom slab then a full \
1380                 block. Same eight-sixteenth tread as the stair and half the pitch, so \
1381                 it is the gentle standard; the run it costs is what the gym weighs it \
1382                 on.",
1383            ),
1384            building(
1385                "storey.low",
1386                MetricValue::Count(5),
1387                "blocks",
1388                Provenance::Derived,
1389                "Floor course, three cells of interior clearance, ceiling course. Taken \
1390                 from the existing cave tileset, every passage and room of which is five \
1391                 blocks tall, so this is the storey the shipped library already has \
1392                 rather than a number invented here.",
1393            ),
1394            building(
1395                "storey.standard",
1396                MetricValue::Count(8),
1397                "blocks",
1398                Provenance::Provisional,
1399                "The storey an interior room of consequence gets: six cells of clearance \
1400                 between courses. A seed, and the walk is what says whether a room this \
1401                 tall reads as generous or merely as far away.",
1402            ),
1403            building(
1404                "storey.hall",
1405                MetricValue::Count(14),
1406                "blocks",
1407                Provenance::Provisional,
1408                "The storey a hall gets, where the height itself is the effect. The seed \
1409                 is deliberately at the point where volume starts costing walking time \
1410                 for nothing, because that is the trade the walk has to judge.",
1411            ),
1412            building(
1413                "size-class.alcove",
1414                MetricValue::SizeClass(SizeClass {
1415                    min_footprint: [4, 4],
1416                    max_footprint: [8, 8],
1417                    min_clearance: 3,
1418                    nominal_traverse_blocks: 6,
1419                }),
1420                "cells",
1421                Provenance::Provisional,
1422                "A place a body stands in rather than crosses: a shrine, a landing, a \
1423                 cell. The smallest rung of the ladder, and the whole ladder's bounds \
1424                 are seeds — what the walk fixes is where one class stops feeling like \
1425                 the next.",
1426            ),
1427            building(
1428                "size-class.room",
1429                MetricValue::SizeClass(SizeClass {
1430                    min_footprint: [8, 8],
1431                    max_footprint: [16, 16],
1432                    min_clearance: 4,
1433                    nominal_traverse_blocks: 12,
1434                }),
1435                "cells",
1436                Provenance::Provisional,
1437                "A place with a purpose and something in it: a guardroom, a chapel, a \
1438                 workshop.",
1439            ),
1440            building(
1441                "size-class.hall",
1442                MetricValue::SizeClass(SizeClass {
1443                    min_footprint: [16, 16],
1444                    max_footprint: [32, 32],
1445                    min_clearance: 8,
1446                    nominal_traverse_blocks: 24,
1447                }),
1448                "cells",
1449                Provenance::Provisional,
1450                "A place a fight or a crowd fits in, and the smallest rung whose height \
1451                 is doing work of its own.",
1452            ),
1453            building(
1454                "size-class.arena",
1455                MetricValue::SizeClass(SizeClass {
1456                    min_footprint: [32, 32],
1457                    max_footprint: [64, 64],
1458                    min_clearance: 12,
1459                    nominal_traverse_blocks: 48,
1460                }),
1461                "cells",
1462                Provenance::Provisional,
1463                "A place built around one encounter, with room to retreat and re-approach.",
1464            ),
1465            building(
1466                "size-class.expanse",
1467                MetricValue::SizeClass(SizeClass {
1468                    min_footprint: [64, 64],
1469                    max_footprint: [128, 128],
1470                    min_clearance: 16,
1471                    nominal_traverse_blocks: 96,
1472                }),
1473                "cells",
1474                Provenance::Provisional,
1475                "A shore, a valley floor, a cavern — a place whose job is that crossing \
1476                 it takes time. The rung most at risk of being a big empty room, which \
1477                 is what the walk is watching for.",
1478            ),
1479            building(
1480                "drop.max-designed-rise",
1481                MetricValue::Count(5),
1482                "blocks",
1483                Provenance::Provisional,
1484                "The deepest fall a designed one-way drop edge may declare. A policy \
1485                 cap, not a physical one: the unarmoured survivable fall beside it in \
1486                 the player half is 22 blocks, and this is far tighter on purpose, \
1487                 because a drop is a topology decision and should not also be a health \
1488                 decision. Five costs two of twenty at full health, which is the seed \
1489                 the walk argues with.",
1490            ),
1491            building(
1492                "pacing.route-blocks-per-minute",
1493                MetricValue::Count(60),
1494                "blocks/minute",
1495                Provenance::Provisional,
1496                "Blocks of route a party gets through per minute of play, once looking, \
1497                 fighting and backtracking are in it. Carried with NO THRESHOLD \
1498                 anywhere until the first walked blockout and the first full playtest \
1499                 calibrate it: a threshold on a number this uncertain would be defending \
1500                 nothing. Its upper bound is the pure-walk figure beside it, which no \
1501                 party achieves.",
1502            ),
1503            building(
1504                "pacing.walk-only-blocks-per-minute",
1505                MetricValue::Count((WALK_SPEED_BLOCKS_PER_SECOND * 60.0) as u32),
1506                "blocks/minute",
1507                Provenance::Derived,
1508                "Walking speed times sixty: what a body covers doing nothing but \
1509                 walking in a straight line. It exists so the route coefficient above \
1510                 has a ceiling that is a fact rather than another guess, and so the \
1511                 ratio between them is the thing the playtest actually measures.",
1512            ),
1513        ];
1514
1515        Metrics {
1516            metrics_version: METRICS_VERSION,
1517            mc_version: crate::blocks::MC_VERSION,
1518            player: player_entries.into_iter().collect(),
1519            building: building_entries.into_iter().collect(),
1520        }
1521    }
1522
1523    /// Resolve a name a **document** wrote to its building entry.
1524    ///
1525    /// This is the **only** path from an authored name to an entry, and it is
1526    /// what makes the table the single authority rather than a suggestion: a
1527    /// name it does not define cannot be resolved, so it cannot compile, so no
1528    /// check downstream ever meets one.
1529    ///
1530    /// The other half of that guarantee is that no key string is spelled outside
1531    /// this module. The entries no document names — the kit grid, the designed-
1532    /// drop cap — are reached through the accessors below rather than by looking
1533    /// the key up in [`Metrics::building`], which is public so that a *reporter*
1534    /// can walk the whole table (`delvec metrics` counts it; the tests iterate
1535    /// it). Reporting is not resolution: a caller that walks every entry cannot
1536    /// name a wrong one, and a caller that wants ONE entry has an accessor and
1537    /// therefore no reason to type a key.
1538    ///
1539    /// # Errors
1540    ///
1541    /// [`UnknownMetric`], which the caller turns into `DW0812` with its own
1542    /// stage and path.
1543    pub fn resolve(&self, kind: MetricKind, named: &str) -> Result<&BuildingEntry, UnknownMetric> {
1544        let key = format!("{}{}", kind.prefix(), named);
1545        self.building
1546            .get(key.as_str())
1547            .ok_or_else(|| UnknownMetric {
1548                kind: kind.noun(),
1549                kind_plural: kind.plural(),
1550                named: named.to_string(),
1551                defined: self.names_of(kind),
1552            })
1553    }
1554
1555    /// The kit grid — the quantum a site-plan box's footprint is a multiple of,
1556    /// and the datum convention that fixes what a declared floor `y` means.
1557    ///
1558    /// One of the entries **no document names**: an author writes a number, not
1559    /// the word `grid`, so it has no place in [`Metrics::resolve`]'s naming
1560    /// vocabulary and would need a [`MetricKind`] whose prefix is the empty
1561    /// string — which would make `names_of` return the whole table. An accessor
1562    /// instead, so the key string still lives here and nowhere else.
1563    ///
1564    /// `None` only if the table stopped defining it, which
1565    /// [`Metrics::self_check`] reports as an internal error.
1566    #[must_use]
1567    pub fn grid(&self, reads: &mut Reads) -> Option<Grid> {
1568        match self.building.get("grid")?.value(reads) {
1569            MetricValue::Grid(g) => Some(*g),
1570            _ => None,
1571        }
1572    }
1573
1574    /// The deepest fall a **designed** one-way drop may declare, in blocks — a
1575    /// policy cap, deliberately tighter than the survivability fact in the
1576    /// player half. See [`Metrics::grid`] for why this is an accessor.
1577    #[must_use]
1578    pub fn max_designed_drop_blocks(&self, reads: &mut Reads) -> Option<u32> {
1579        match self.building.get("drop.max-designed-rise")?.value(reads) {
1580            MetricValue::Count(n) => Some(*n),
1581            _ => None,
1582        }
1583    }
1584
1585    /// The **widest** standard opening in the table, in cells — the floor a
1586    /// contact seam's span must exceed (spec-0053 §4).
1587    ///
1588    /// Derived from the table rather than seeded, and that is the whole of why
1589    /// the floor is honest: anything at or under this width **could have been a
1590    /// portal**, so a doorway declared a contact to dodge the standard set is
1591    /// refused by its own width. A seeded floor would be a number an author
1592    /// could argue with; this one is a consequence of the standard set, and it
1593    /// moves when the standard set moves.
1594    ///
1595    /// It walks every opening rather than naming one, so a broader standard
1596    /// landing tomorrow raises the floor with no edit here — the failure mode a
1597    /// hand-named `opening.gateway` would have is that the floor silently stops
1598    /// being the broadest the day a broader one is added.
1599    ///
1600    /// `None` only if the table defines no opening at all, which
1601    /// [`Metrics::self_check`] reports as an internal error.
1602    #[must_use]
1603    pub fn broadest_opening_width(&self, reads: &mut Reads) -> Option<u32> {
1604        let mut widest: Option<u32> = None;
1605        for name in self.names_of(MetricKind::Opening) {
1606            let Ok(entry) = self.resolve(MetricKind::Opening, name) else {
1607                continue;
1608            };
1609            if let MetricValue::Opening(o) = entry.value(reads) {
1610                widest = Some(widest.map_or(o.width, |w: u32| w.max(o.width)));
1611            }
1612        }
1613        widest
1614    }
1615
1616    /// Every name defined for a kind, in table order.
1617    #[must_use]
1618    pub fn names_of(&self, kind: MetricKind) -> Vec<&'static str> {
1619        self.building
1620            .keys()
1621            .filter_map(|k| k.strip_prefix(kind.prefix()))
1622            .collect()
1623    }
1624
1625    /// The `DW0813` notice for a run, or `None` when no verdict rested on an
1626    /// unwalked standard.
1627    ///
1628    /// `None` at a zero binding is the calibrated end state and not a vacuity:
1629    /// the line's job is to say that a green rests on a seed, and once the gym
1630    /// has walked every entry a run reads there is nothing left for it to say.
1631    /// The distinguishable failure — a run that read NOTHING — is reported by
1632    /// [`Reads::binding`], which every caller states whether or not this returns
1633    /// a line.
1634    #[must_use]
1635    pub fn notice(&self, reads: &Reads, stage: &str) -> Option<Diagnostic> {
1636        let provisional = reads.provisional();
1637        if provisional.is_empty() {
1638            return None;
1639        }
1640        let binding = reads.binding();
1641        Some(Diagnostic::warning(
1642            DW_METRIC_PROVISIONAL,
1643            stage,
1644            "",
1645            format!(
1646                "{n} of the {read} building metric(s) this run read are provisional — the \
1647                 metrics gym has not walked them: {names}. The checks still ran and still \
1648                 refuse; what is unproven is the number they refused against.",
1649                n = binding.provisional,
1650                read = binding.read,
1651                names = provisional.join(", "),
1652            ),
1653        ))
1654    }
1655
1656    /// Check the table against itself and against the player half.
1657    ///
1658    /// These are verdicts, and they read building metrics through a [`Reads`], so
1659    /// they are also what gives `DW0813` a live binding at this version: `delvec
1660    /// metrics` runs them, and the notice names the seeds the consistency verdict
1661    /// rested on. They are the reason the mechanism is demonstrable now rather
1662    /// than at the round that adds the documents.
1663    ///
1664    /// A violation is an **internal error**, not a diagnostic: the table is
1665    /// engine data, so an inconsistent one is a defect in this file and not in
1666    /// anybody's campaign, and there is no author to address a refusal to.
1667    #[must_use]
1668    pub fn self_check(&self) -> SelfCheck {
1669        let mut reads = Reads::new();
1670        let mut failures: Vec<String> = Vec::new();
1671        let mut checked = 0usize;
1672
1673        let floor_w = u64::from(passable_width_cells());
1674        let floor_h = u64::from(passable_clearance_cells());
1675
1676        // A place that is a route must be spellable at all. Zero way classes is
1677        // the state spec-0053 was written to end — the metrics gym reported
1678        // `corridor.min-width` and `corridor.min-clearance` unreachable because
1679        // no document could name a place that is not a box with a size class —
1680        // so an empty way vocabulary is an internal error rather than a table
1681        // that happens to be short one kind.
1682        checked += 1;
1683        if self.names_of(MetricKind::WayClass).is_empty() {
1684            failures.push(
1685                "the table defines no way class, so no document can state a place that is \
1686                 a route"
1687                    .to_string(),
1688            );
1689        }
1690
1691        // The contact floor is derived from the standard opening set (spec-0053
1692        // §4), so an empty set would make that floor `None` and the refusal it
1693        // is the floor for unable to separate a doorway from a front.
1694        checked += 1;
1695        if self.broadest_opening_width(&mut reads).is_none() {
1696            failures.push(
1697                "the table defines no standard opening, so a contact seam's width floor — \
1698                 the width a front must exceed to be a front rather than a door — cannot \
1699                 be derived"
1700                    .to_string(),
1701            );
1702        }
1703
1704        let quantum = match self.grid(&mut reads) {
1705            Some(g) => {
1706                checked += 1;
1707                if g.quantum == 0 {
1708                    failures.push("the kit grid's quantum is zero".to_string());
1709                }
1710                g.quantum
1711            }
1712            None => {
1713                failures.push("the table defines no kit `grid`".to_string());
1714                1
1715            }
1716        };
1717
1718        for (key, entry) in &self.building {
1719            match entry.value(&mut reads) {
1720                MetricValue::Opening(o) => {
1721                    checked += 1;
1722                    if u64::from(o.width) < floor_w || u64::from(o.height) < floor_h {
1723                        failures.push(format!(
1724                            "`{key}` is {}×{}, which no standing body passes ({floor_w}×{floor_h} \
1725                             is the floor)",
1726                            o.width, o.height
1727                        ));
1728                    }
1729                }
1730                MetricValue::Pitch(p) => {
1731                    checked += 1;
1732                    if p.step_16 > MAX_AUTO_STEP_16 {
1733                        failures.push(format!(
1734                            "`{key}` presents a tread of {}/16, over the {MAX_AUTO_STEP_16}/16 \
1735                             walk-up budget, so it is climbed by jumping and is not a standard \
1736                             pitch",
1737                            p.step_16
1738                        ));
1739                    }
1740                    if p.rise == 0 || p.run == 0 {
1741                        failures.push(format!("`{key}` has a zero rise or run"));
1742                    }
1743                }
1744                MetricValue::SizeClass(c) => {
1745                    checked += 1;
1746                    for (axis, lo, hi) in [
1747                        ("x", c.min_footprint[0], c.max_footprint[0]),
1748                        ("z", c.min_footprint[1], c.max_footprint[1]),
1749                    ] {
1750                        if lo > hi {
1751                            failures
1752                                .push(format!("`{key}` has a {axis} minimum above its maximum"));
1753                        }
1754                        if lo % quantum != 0 || hi % quantum != 0 {
1755                            failures.push(format!(
1756                                "`{key}` bounds its {axis} footprint at {lo}..{hi}, which is not \
1757                                 on the kit grid's quantum of {quantum}"
1758                            ));
1759                        }
1760                    }
1761                    if u64::from(c.min_clearance) < floor_h {
1762                        failures.push(format!(
1763                            "`{key}` allows a clearance of {}, under the passable floor of \
1764                             {floor_h}",
1765                            c.min_clearance
1766                        ));
1767                    }
1768                    if c.nominal_traverse_blocks == 0 {
1769                        failures.push(format!("`{key}` has a nominal traverse of zero"));
1770                    }
1771                }
1772                MetricValue::WayClass(w) => {
1773                    checked += 1;
1774                    // The two floors the freestanding `corridor.min-*` entries
1775                    // used to be checked at, re-asserted here against every way
1776                    // class rather than against the one that inherited them: a
1777                    // designed minimum is a comfort judgement and a standard
1778                    // under the physical passable size would be a standard
1779                    // nothing can use, which is true of a road exactly as it is
1780                    // of a corridor.
1781                    if u64::from(w.min_width) < floor_w {
1782                        failures.push(format!(
1783                            "`{key}` allows a width of {}, under the `passable.width` floor of \
1784                             {floor_w}",
1785                            w.min_width
1786                        ));
1787                    }
1788                    if u64::from(w.min_clearance) < floor_h {
1789                        failures.push(format!(
1790                            "`{key}` allows a clearance of {}, under the `passable.clearance` \
1791                             floor of {floor_h}",
1792                            w.min_clearance
1793                        ));
1794                    }
1795                    if w.min_width > w.max_width {
1796                        failures.push(format!(
1797                            "`{key}` bounds its width at {}..{}, a minimum above its maximum",
1798                            w.min_width, w.max_width
1799                        ));
1800                    }
1801                    // `max_width` is the elongation floor as well as the widest
1802                    // cross-section, and a box's horizontal extents are
1803                    // multiples of the kit quantum (`DW0825`). A `max_width` off
1804                    // the quantum is therefore a bound no plan can draw a way
1805                    // AT, which makes the widest member of the class
1806                    // uninstantiable and the gym unable to rule on it. The
1807                    // narrow bound is deliberately NOT held to the quantum: the
1808                    // corridor's inherited floor of 2 sits under a quantum of 4
1809                    // and which of those two provisional numbers moves is the
1810                    // walk's to decide, not this file's.
1811                    if !w.max_width.is_multiple_of(quantum) {
1812                        failures.push(format!(
1813                            "`{key}` bounds its width at {}, which is not on the kit grid's \
1814                             quantum of {quantum}, so no box can be drawn at the widest member \
1815                             of the class",
1816                            w.max_width
1817                        ));
1818                    }
1819                }
1820                _ => {}
1821            }
1822        }
1823
1824        for (key, floor) in [
1825            ("storey.low", floor_h + 2),
1826            ("storey.standard", floor_h + 2),
1827            ("storey.hall", floor_h + 2),
1828        ] {
1829            let Some(entry) = self.building.get(key) else {
1830                failures.push(format!("the table defines no `{key}`"));
1831                continue;
1832            };
1833            checked += 1;
1834            if let MetricValue::Count(n) = entry.value(&mut reads)
1835                && u64::from(*n) < floor
1836            {
1837                failures.push(format!(
1838                    "`{key}` is {n} blocks, which leaves no passable interior between a \
1839                     floor course and a ceiling course ({floor} is the floor)"
1840                ));
1841            }
1842        }
1843
1844        // The policy cap is deliberately tighter than the physical one. A cap
1845        // that reached the survivability ceiling would not be a policy.
1846        if let Some(n) = self.max_designed_drop_blocks(&mut reads) {
1847            checked += 1;
1848            let physical = unarmoured_survivable_fall_blocks();
1849            if f64::from(n) >= physical {
1850                failures.push(format!(
1851                    "`drop.max-designed-rise` is {n} blocks, at or past the unarmoured \
1852                     survivable fall of {physical}, so it is not a policy cap at all"
1853                ));
1854            }
1855        }
1856
1857        // A party cannot out-pace a body walking in a straight line.
1858        if let (Some(route), Some(walk)) = (
1859            self.building.get("pacing.route-blocks-per-minute"),
1860            self.building.get("pacing.walk-only-blocks-per-minute"),
1861        ) {
1862            checked += 1;
1863            if let (MetricValue::Count(r), MetricValue::Count(w)) =
1864                (route.value(&mut reads), walk.value(&mut reads))
1865                && r > w
1866            {
1867                failures.push(format!(
1868                    "`pacing.route-blocks-per-minute` is {r}, over the pure-walk ceiling of {w}"
1869                ));
1870            }
1871        }
1872
1873        SelfCheck {
1874            binding: SelfCheckBinding {
1875                invariants: checked,
1876                entries: self.building.len(),
1877                reads: reads.binding(),
1878            },
1879            reads,
1880            failures,
1881        }
1882    }
1883}
1884
1885/// What [`Metrics::self_check`] examined and what it found.
1886#[derive(Debug, Clone, PartialEq)]
1887pub struct SelfCheck {
1888    /// The ledger the verdicts read through, for [`Metrics::notice`].
1889    pub reads: Reads,
1890    /// What the run bound to. Stated whether or not anything failed, because a
1891    /// check that examined nothing is a finding and not a pass.
1892    pub binding: SelfCheckBinding,
1893    /// Inconsistencies, each a whole sentence. Non-empty is an internal error.
1894    pub failures: Vec<String>,
1895}
1896
1897/// [`Metrics::self_check`]'s binding count.
1898#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1899pub struct SelfCheckBinding {
1900    /// Invariants evaluated.
1901    pub invariants: usize,
1902    /// Building entries in the table.
1903    pub entries: usize,
1904    /// What those invariants read.
1905    pub reads: ReadBinding,
1906}
1907
1908/// The table rendered for export: every entry with its value, unit, provenance
1909/// and note, plus the halves' own counts.
1910///
1911/// Separate from [`Metrics`]'s own `Serialize` because the building half's value
1912/// is private — an export is the one consumer that reads a number without
1913/// resting a verdict on it.
1914#[must_use]
1915pub fn export(metrics: &Metrics) -> serde_json::Value {
1916    let mut player = serde_json::Map::new();
1917    for (k, e) in &metrics.player {
1918        player.insert((*k).to_string(), serde_json::json!(e));
1919    }
1920    let mut building = serde_json::Map::new();
1921    let mut uncalibrated = 0usize;
1922    for (k, e) in &metrics.building {
1923        if !e.calibrated {
1924            uncalibrated += 1;
1925        }
1926        building.insert(
1927            (*k).to_string(),
1928            serde_json::json!({
1929                "value": e.value_for_display(),
1930                "unit": e.unit,
1931                "provenance": e.provenance,
1932                "calibrated": e.calibrated,
1933                "note": e.note,
1934            }),
1935        );
1936    }
1937    serde_json::json!({
1938        "metrics_version": metrics.metrics_version,
1939        "mc_version": metrics.mc_version,
1940        "counts": {
1941            "player": metrics.player.len(),
1942            "building": metrics.building.len(),
1943            "uncalibrated": uncalibrated,
1944        },
1945        "player": player,
1946        "building": building,
1947    })
1948}
1949
1950#[cfg(test)]
1951mod tests {
1952    use super::*;
1953
1954    #[test]
1955    fn the_table_is_consistent_with_itself_and_with_the_player_half() {
1956        let m = Metrics::table();
1957        let check = m.self_check();
1958        assert!(
1959            check.failures.is_empty(),
1960            "the shipped metrics table is inconsistent: {:?}",
1961            check.failures
1962        );
1963        assert!(
1964            check.binding.invariants > 0,
1965            "a self-check that evaluated nothing is vacuous, not a pass"
1966        );
1967    }
1968
1969    #[test]
1970    fn every_building_entry_lands_uncalibrated() {
1971        let m = Metrics::table();
1972        assert!(!m.building.is_empty(), "the building half is empty");
1973        for (k, e) in &m.building {
1974            assert!(
1975                !e.calibrated,
1976                "`{k}` claims to be calibrated, but the metrics gym has not been walked"
1977            );
1978        }
1979    }
1980
1981    #[test]
1982    fn a_verdict_that_reads_a_seed_raises_dw0813_naming_it() {
1983        let m = Metrics::table();
1984        let check = m.self_check();
1985        assert!(
1986            check.binding.reads.provisional > 0,
1987            "the self-check read no provisional entry, so DW0813 binds to nothing"
1988        );
1989        let d = m
1990            .notice(&check.reads, "metrics")
1991            .expect("a run that read a seed owes the notice");
1992        assert_eq!(d.code, "DW0813");
1993        assert_eq!(d.severity, crate::diagnostic::Severity::Warning);
1994        for name in check.reads.provisional() {
1995            assert!(
1996                d.message.contains(name),
1997                "the notice must name `{name}`, the seed a verdict rested on"
1998            );
1999        }
2000    }
2001
2002    #[test]
2003    fn a_run_that_read_nothing_provisional_gets_no_notice() {
2004        let m = Metrics::table();
2005        let reads = Reads::new();
2006        assert!(m.notice(&reads, "metrics").is_none());
2007        assert_eq!(reads.binding().read, 0);
2008    }
2009
2010    #[test]
2011    fn an_undefined_name_is_dw0812_and_names_what_is_defined() {
2012        let m = Metrics::table();
2013        let err = m
2014            .resolve(MetricKind::SizeClass, "cathedral")
2015            .expect_err("`cathedral` is not a size class");
2016        let d = err.diagnostic("layout-graph", "/nodes/0/size_class");
2017        assert_eq!(d.code, "DW0812");
2018        assert!(d.message.contains("cathedral"));
2019        assert!(d.message.contains("room"), "the defined set is named");
2020        assert_eq!(d.stage, "layout-graph");
2021        assert_eq!(d.path, "/nodes/0/size_class");
2022    }
2023
2024    #[test]
2025    fn every_kind_resolves_at_least_one_defined_name() {
2026        let m = Metrics::table();
2027        for kind in [
2028            MetricKind::Opening,
2029            MetricKind::Pitch,
2030            MetricKind::SizeClass,
2031            MetricKind::Storey,
2032        ] {
2033            let names = m.names_of(kind);
2034            assert!(
2035                !names.is_empty(),
2036                "{} resolves nothing, so DW0812 would refuse every name",
2037                kind.noun()
2038            );
2039            for n in names {
2040                assert!(m.resolve(kind, n).is_ok(), "`{n}` does not resolve");
2041            }
2042        }
2043    }
2044
2045    // --- a body has a width -------------------------------------------------
2046
2047    /// The fact the whole rule rests on, written down where it can be checked: a
2048    /// box selector spelled `x=lo,dx=hi-lo` is the world volume `[lo, hi+1]`, so
2049    /// `dx=0` is one whole block and never a plane.
2050    #[test]
2051    fn a_box_selector_spans_the_whole_of_its_last_cell() {
2052        assert_eq!(
2053            selection_aabb([7, 63, 20], [11, 67, 24]),
2054            ([7.0, 63.0, 20.0], [12.0, 68.0, 25.0])
2055        );
2056        let (lo, hi) = selection_aabb([0, 0, 0], [0, 0, 0]);
2057        assert_eq!((lo, hi), ([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]));
2058    }
2059
2060    /// **The measurement this rule was written from.** A wave's spider, 1.4 wide,
2061    /// walked to `x = 12.59` beside a pit whose box ends at `x = 12.0` and was
2062    /// killed there — its hitbox reaches `11.89`. Its feet cell is 12, which is
2063    /// outside the box, so every cell-shaped reading in the engine called that
2064    /// position safe.
2065    #[test]
2066    fn a_body_beside_a_face_is_inside_the_volume_and_its_cell_is_not() {
2067        let west_pit = ([7, 63, 20], [11, 67, 24]);
2068        let spider = Body::new(1.4, 1.4);
2069        assert!(
2070            body_meets_volume([12.59, 65.0, 21.02], spider, west_pit.0, west_pit.1),
2071            "hitbox 11.89..13.29 against a box face at 12.0"
2072        );
2073        // The cell it stands in is not one of the volume's cells.
2074        assert!(!(west_pit.0[0]..=west_pit.1[0]).contains(&12));
2075        // A player at the same cell's CENTRE is clear of it — 12.2..12.8 — which
2076        // is why the seat rule and the walk rule are two different readings.
2077        assert!(!body_meets_volume(
2078            [12.5, 65.0, 21.5],
2079            Body::PLAYER,
2080            west_pit.0,
2081            west_pit.1
2082        ));
2083        // …and the walk rule refuses that cell anyway, because a walker does not
2084        // stand at cell centres.
2085        assert!(cell_can_meet_volume(
2086            [12, 65, 21],
2087            Body::PLAYER,
2088            west_pit.0,
2089            west_pit.1
2090        ));
2091    }
2092
2093    /// The touching case is NOT an intersection, exactly as vanilla's own
2094    /// `AABB::intersects` is strict: a body standing on a volume's top face with
2095    /// its feet at the face is clear of it.
2096    #[test]
2097    fn a_face_that_exactly_touches_is_not_inside() {
2098        let v = ([0, 0, 0], [0, 0, 0]);
2099        assert!(!body_meets_volume([0.5, 1.0, 0.5], Body::PLAYER, v.0, v.1));
2100        assert!(body_meets_volume([0.5, 0.999, 0.5], Body::PLAYER, v.0, v.1));
2101    }
2102
2103    /// The keep-out box is a DERIVATION, not a constant one: it widens with the
2104    /// body. Horizontally every body up to two blocks wide reaches exactly one
2105    /// cell (half a width can never cross two cell boundaries at once);
2106    /// vertically a body taller than two blocks reaches one cell further down
2107    /// than a player does, which is the only place the bodies in this engine's
2108    /// dims table differ.
2109    #[test]
2110    fn the_keep_out_box_widens_with_the_body() {
2111        let v = ([10, 60, 10], [12, 62, 12]);
2112        assert_eq!(
2113            keep_out_box(Body::PLAYER, v.0, v.1),
2114            ([9, 59, 9], [13, 62, 13])
2115        );
2116        // 1.4 wide: the same ring. Half a width is 0.7, which crosses one
2117        // boundary and not two.
2118        assert_eq!(
2119            keep_out_box(Body::new(1.4, 1.4), v.0, v.1),
2120            ([9, 59, 9], [13, 62, 13])
2121        );
2122        // 2.9 tall (a warden): one cell further DOWN, because its head reaches
2123        // into the volume from a cell a player's head does not.
2124        assert_eq!(
2125            keep_out_box(Body::new(0.9, 2.9), v.0, v.1),
2126            ([9, 58, 9], [13, 62, 13])
2127        );
2128        // Standing ON the volume's top cell is safe for every body: the feet
2129        // start where the volume ends.
2130        assert!(!cell_can_meet_volume(
2131            [11, 63, 11],
2132            Body::new(0.9, 2.9),
2133            v.0,
2134            v.1
2135        ));
2136    }
2137
2138    #[test]
2139    fn the_derived_player_values_are_the_arithmetic_their_notes_claim() {
2140        assert!((walk_ticks_per_block() - 20.0 / 4.317).abs() < f64::EPSILON);
2141        assert!((walk_ticks_per_block() - 4.633).abs() < 0.001);
2142        assert_eq!(unarmoured_survivable_fall_blocks(), 22.0);
2143        assert_eq!(passable_width_cells(), 1);
2144        assert_eq!(passable_clearance_cells(), 2);
2145    }
2146
2147    #[test]
2148    fn the_jump_reach_runs_from_the_apex_to_the_deepest_survivable_fall_and_never_narrows() {
2149        assert_eq!(JUMP_REACH.first().map(|r| r.0), Some(1));
2150        assert_eq!(
2151            JUMP_REACH.last().map(|r| r.0),
2152            Some(-(unarmoured_survivable_fall_blocks() as i64))
2153        );
2154        for w in JUMP_REACH.windows(2) {
2155            assert_eq!(w[1].0, w[0].0 - 1, "one row per whole block of rise");
2156            assert!(
2157                w[1].1 >= w[0].1,
2158                "a deeper landing never admits a narrower gap: {w:?}"
2159            );
2160        }
2161        assert_eq!(jump_max_gap(MAX_JUMP_RISE_16), Some(3));
2162        assert_eq!(jump_max_gap(MAX_JUMP_RISE_16 + 1), None);
2163        assert_eq!(jump_max_gap(0), Some(4));
2164        assert_eq!(jump_max_gap(-24), Some(4), "a drop of 1.5 reads the -1 row");
2165        assert_eq!(jump_max_gap(-22 * FULL_16), Some(9));
2166        assert_eq!(jump_max_gap(-22 * FULL_16 - 1), None);
2167    }
2168}