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