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
75crate::dw_code! {
76    /// `DW0812`: a document names a metrics entry the table does not define — a
77    /// seam `opening`, a `pitch` or a storey 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 = 4;
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/// The vertical speed a body on a climbable is **given**, in blocks per tick,
404/// while it pushes against something or holds jump (spec-0099).
405///
406/// **Cited**, the pinned jar: `LivingEntity.handleRelativeFrictionAndCalculateMovement`
407/// sets the movement's `y` to `0.2` when `(horizontalCollision || jumping) &&
408/// onClimbable()`. The jump half is what lets a body climb a vine that hangs in
409/// open air, and what the harness bot holds.
410pub const CLIMB_SET_SPEED_BLOCKS_PER_TICK: f64 = 0.2;
411
412/// The fastest a body on a climbable **slides down**, and the fastest it moves
413/// sideways on one, in blocks per tick (spec-0099).
414///
415/// **Cited**, the pinned jar: `LivingEntity.handleOnClimbable` clamps `x` and `z`
416/// to `±0.15` and `y` to at least `-0.15`, and resets the fall distance on every
417/// tick the body is on one — so a body that reaches a climbable stops falling
418/// and takes no damage for the fall above it. A player who sneaks holds still
419/// (`y` is set to 0 while `isSuppressingSlidingDownLadder()`, which a player
420/// answers with `isShiftKeyDown()`), on every climbable but scaffolding.
421pub const CLIMB_SLIDE_BLOCKS_PER_TICK: f64 = 0.15;
422
423/// The air drag vanilla applies to a body's vertical speed each tick, and the
424/// gravity it subtracts first: `vy' = (vy − g)·k` (**cited**,
425/// `LivingEntity.travelInAir`: the `0.98f` factor; `g` is the `gravity`
426/// attribute, `0.08` for a player). [`POLL_FALL_BLOCKS_PER_TICK`] is this law's
427/// fixed point, measured.
428pub const AIR_DRAG: f64 = 0.98;
429
430/// See [`AIR_DRAG`].
431pub const GRAVITY_BLOCKS_PER_TICK2: f64 = 0.08;
432
433/// The speed a body climbs at, in blocks per tick: the set speed, less one
434/// tick of gravity, dragged — `(0.2 − 0.08) × 0.98 = 0.1176`, 2.35 blocks a
435/// second. Derived, because the movement a tick applies is the speed the
436/// previous tick left after gravity and drag.
437#[must_use]
438pub fn climb_blocks_per_tick() -> f64 {
439    (CLIMB_SET_SPEED_BLOCKS_PER_TICK - GRAVITY_BLOCKS_PER_TICK2) * AIR_DRAG
440}
441
442/// **How far a body may fall onto a climbable and still be caught by it**, in
443/// whole blocks (spec-0099 §3.5).
444///
445/// A climbable holds a body only on a tick that begins with the body's FEET in
446/// its cell (`onClimbable()` reads the block at `blockPosition()`). A body
447/// falling less than one block per tick cannot pass a one-block cell between
448/// two ticks, so it is caught; one falling faster can. Derived from the fall law
449/// ([`AIR_DRAG`], [`GRAVITY_BLOCKS_PER_TICK2`]): the distance fallen through the
450/// last tick whose step is still under one block, floored. Seven blocks: the
451/// fourteenth tick moves 0.966 and has fallen 7.56 in all, the fifteenth moves
452/// 1.025.
453#[must_use]
454pub fn climb_catch_fall_blocks() -> u32 {
455    let (mut v, mut fallen) = (0.0_f64, 0.0_f64);
456    loop {
457        let next = (v + GRAVITY_BLOCKS_PER_TICK2) * AIR_DRAG;
458        if next >= 1.0 {
459            return fallen.floor() as u32;
460        }
461        v = next;
462        fallen += v;
463    }
464}
465
466/// Ticks a walking player spends crossing one block on the flat.
467///
468/// Derived rather than stored, because both operands are facts and nothing
469/// downstream decides a route on this number — it is the pacing denominator.
470#[must_use]
471pub fn walk_ticks_per_block() -> f64 {
472    TICKS_PER_SECOND / WALK_SPEED_BLOCKS_PER_SECOND
473}
474
475/// The largest fall an unarmoured player at full health survives, in blocks.
476///
477/// `ceil(d − 3) < 20` holds up to `d = 22`, which lands on one half-heart; 23
478/// blocks deals 20 and kills. Derived from [`FALL_DAMAGE_ONSET_BLOCKS`] and
479/// [`PLAYER_MAX_HEALTH`] so that moving either moves this, and it exists so the
480/// fall a designed drop may take has a physical ceiling (`DW0831`).
481#[must_use]
482pub fn unarmoured_survivable_fall_blocks() -> f64 {
483    (FALL_DAMAGE_ONSET_BLOCKS + PLAYER_MAX_HEALTH - 1.0).floor()
484}
485
486/// Horizontal cells a standing body needs to pass: `ceil(0.6)`.
487#[must_use]
488pub fn passable_width_cells() -> u32 {
489    PLAYER_WIDTH.ceil() as u32
490}
491
492/// Vertical cells a standing body needs to pass: `ceil(1.8)`.
493///
494/// This is a **player** metric and the spec's building half listed it, which is
495/// the correction worth naming: the width and clearance at which a body can pass
496/// at all are functions of the collision box, so no walk can change them and
497/// `calibrated` would mean nothing on them.
498#[must_use]
499pub fn passable_clearance_cells() -> u32 {
500    PLAYER_HEIGHT.ceil() as u32
501}
502
503// ---------------------------------------------------------------------------
504// A body has a width — the one answer to "does this body meet this volume"
505// ---------------------------------------------------------------------------
506
507/// A body's standing collision box: `width × height × width`, centred
508/// horizontally on the body's position and rising from its feet.
509///
510/// It lives here for the reason [`step_allowed`] does. Three consumers ask the
511/// same geometric question of the same object class — the routing model asks
512/// which cells a walker may occupy beside a killing volume, the posted-place
513/// proof asks whether a body seated at a declared cell is inside one, and the
514/// emitter formats the runtime selector that decides both at play time — and a
515/// question with three answers is a question three gates can disagree about.
516/// The disagreement was not hypothetical: every proof reasoned in whole cells
517/// while the runtime selected on hitbox intersection, so the engine routed a
518/// player along a cell whose occupant the volume kills, chose that same cell as
519/// the "safe" lip a stake stands on, and reported both green.
520#[derive(Debug, Clone, Copy, PartialEq)]
521pub struct Body {
522    /// Collision-box width and depth, in blocks.
523    pub width: f64,
524    /// Collision-box height, in blocks.
525    pub height: f64,
526}
527
528impl Body {
529    /// The player's own body, from the constants above rather than from a
530    /// second pair of literals.
531    pub const PLAYER: Body = Body {
532        width: PLAYER_WIDTH,
533        height: PLAYER_HEIGHT,
534    };
535
536    /// A body of the given standing hitbox.
537    #[must_use]
538    pub fn new(width: f64, height: f64) -> Body {
539        Body { width, height }
540    }
541
542    /// Half the width — the reach of the box either side of the body's own
543    /// position.
544    #[must_use]
545    pub fn half_width(&self) -> f64 {
546        self.width / 2.0
547    }
548}
549
550/// The world-space AABB vanilla builds from a box selector over the inclusive
551/// cell box `lo..=hi`.
552///
553/// **This is a fact about the game, and it is what the emitter writes.** A box
554/// selector is spelled `x=lo,dx=hi-lo,…` (`emit::box_selector_args`), and
555/// vanilla's `EntitySelectorParser` turns `x`/`dx` into the half-open span
556/// `[x, x+dx+1]` — which is why `dx=0` selects the whole of one block rather
557/// than a plane. So the inclusive cell box `lo..=hi` is the world volume
558/// `[lo, hi+1]`, and an entity is selected when its **hitbox intersects** it,
559/// never when the cell it stands in is one of the box's cells.
560///
561/// Stated as a [`Provenance::VanillaRule`] would be: this repository has not
562/// measured the parser, but it has measured the consequence — bodies whose feet
563/// cell lies outside a declared volume are killed by it, at the two faces this
564/// arithmetic predicts.
565#[must_use]
566pub fn selection_aabb(lo: [i32; 3], hi: [i32; 3]) -> ([f64; 3], [f64; 3]) {
567    (
568        [lo[0] as f64, lo[1] as f64, lo[2] as f64],
569        [hi[0] as f64 + 1.0, hi[1] as f64 + 1.0, hi[2] as f64 + 1.0],
570    )
571}
572
573/// Does a body standing with its feet at `feet` meet the volume `lo..=hi`?
574///
575/// `feet` is the position vanilla keeps for an entity: the horizontal centre of
576/// its collision box, at the box's base. So the body occupies
577/// `[x ± w/2] × [y, y + h] × [z ± w/2]`, and the answer is whether that
578/// intersects [`selection_aabb`]. The comparison is strict on both sides, as
579/// vanilla's `AABB::intersects` is: a body whose face exactly touches the
580/// volume's face is not inside it.
581///
582/// This is the **exact** reading, for a body whose position is known — a summon
583/// writes literal coordinates, and `compiler::nav::cell_center` is the one place
584/// that says which coordinates a body seated on a cell gets. For a body that may
585/// stand anywhere within a cell, ask [`cell_can_meet_volume`] instead.
586#[must_use]
587pub fn body_meets_volume(feet: [f64; 3], body: Body, lo: [i32; 3], hi: [i32; 3]) -> bool {
588    let (vlo, vhi) = selection_aabb(lo, hi);
589    let half = body.half_width();
590    let blo = [feet[0] - half, feet[1], feet[2] - half];
591    let bhi = [feet[0] + half, feet[1] + body.height, feet[2] + half];
592    (0..3).all(|i| blo[i] < vhi[i] && bhi[i] > vlo[i])
593}
594
595/// **The inclusive cell box a body of these dimensions must keep out of** — the
596/// cells from which its hitbox can reach the volume `lo..=hi`.
597///
598/// A walker's cell does not fix its position: a body whose feet block is `c`
599/// stands anywhere in `[c, c+1)` horizontally, so its box spans
600/// `[c - w/2, c + 1 + w/2)`. The cells that can meet the volume are therefore
601/// the volume's own, widened by however many cells half a width reaches into —
602/// one, for every body in the engine's dims table, and the derivation rather
603/// than the constant is what is written here so a wider body widens it.
604///
605/// Vertically the body's feet sit at the cell's own floor and it rises `height`
606/// blocks, so the box reaches `ceil(height)` cells down from the volume's floor
607/// and not at all above its ceiling: standing **on top** of a volume is safe,
608/// standing under one with your head in it is not.
609///
610/// The direction is deliberate and is the opposite of
611/// `compiler::reach::ReachCompletion::certainly_completes_from`, which asks the
612/// same geometry about a volume that GRANTS something and so demands the certain
613/// case. Here the volume kills, so the refusal takes the possible case: a
614/// generous rule is safe for a reward and unsound for a hazard.
615#[must_use]
616pub fn keep_out_box(body: Body, lo: [i32; 3], hi: [i32; 3]) -> ([i32; 3], [i32; 3]) {
617    let half = body.half_width();
618    let mut out_lo = [0i32; 3];
619    let mut out_hi = [0i32; 3];
620    for i in [0usize, 2] {
621        // `c + 1 + half > lo` for some c, and `c - half < hi + 1`.
622        out_lo[i] = (lo[i] as f64 - 1.0 - half).floor() as i32 + 1;
623        out_hi[i] = (hi[i] as f64 + 1.0 + half).ceil() as i32 - 1;
624    }
625    // `c + height > lo_y` (feet at the cell floor), and `c <= hi_y` — a body
626    // whose feet are at `hi_y + 1` starts exactly where the volume ends.
627    out_lo[1] = (lo[1] as f64 - body.height).floor() as i32 + 1;
628    out_hi[1] = hi[1];
629    (out_lo, out_hi)
630}
631
632/// Can a body whose feet block is `cell` meet the volume `lo..=hi` from **some**
633/// position inside that cell? The refusing reading — see [`keep_out_box`].
634///
635/// Its feet are taken at the cell's own floor. A body standing on a partial
636/// block stands lower than that, and a caller that knows where its feet are asks
637/// [`feet_can_meet_volume`] instead; this is that function at `cell.y * 16`.
638#[must_use]
639pub fn cell_can_meet_volume(cell: [i32; 3], body: Body, lo: [i32; 3], hi: [i32; 3]) -> bool {
640    feet_can_meet_volume(cell, i64::from(cell[1]) * 16, body, lo, hi)
641}
642
643/// Can a body whose feet block is `cell`, with its feet at `feet_16` sixteenths
644/// of a block (absolute height), meet the volume `lo..=hi` from **some**
645/// horizontal position inside that cell?
646///
647/// Horizontally it is [`keep_out_box`]'s reading. Vertically the body's box is
648/// `[feet, feet + height)` and the volume's is `[lo.y, hi.y + 1)`, compared
649/// strictly as vanilla's `AABB::intersects` is. The feet are an argument because
650/// the cell does not fix them: a body standing on a bottom slab, a soul-sand
651/// floor or an upward dripstone tip has its feet below its cell's floor — 0.6875
652/// into the tip's own cell — and its box reaches a volume drawn in the course it
653/// stands on, which a reading from the cell floor misses. At `feet_16 =
654/// cell.y * 16` it answers exactly what [`keep_out_box`] does.
655#[must_use]
656pub fn feet_can_meet_volume(
657    cell: [i32; 3],
658    feet_16: i64,
659    body: Body,
660    lo: [i32; 3],
661    hi: [i32; 3],
662) -> bool {
663    let (klo, khi) = keep_out_box(body, lo, hi);
664    if ![0usize, 2]
665        .iter()
666        .all(|&i| klo[i] <= cell[i] && cell[i] <= khi[i])
667    {
668        return false;
669    }
670    let feet = feet_16 as f64 / 16.0;
671    feet < f64::from(hi[1]) + 1.0 && feet + body.height > f64::from(lo[1])
672}
673
674// ---------------------------------------------------------------------------
675// Entry shapes
676// ---------------------------------------------------------------------------
677
678/// Where a number came from. The four are not interchangeable — see the module
679/// docs.
680#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
681#[serde(rename_all = "kebab-case")]
682pub enum Provenance {
683    /// The number **is** a constant this module defines and the rest of the
684    /// workspace imports. One definition, so nothing to drift from.
685    EngineConstant,
686    /// A stated rule of pinned Minecraft Java 1.21.11 that no engine constant
687    /// held before this table, and that this repository has **not** measured on
688    /// a running server. The note names the rule so the claim is checkable.
689    VanillaRule,
690    /// Computed from other entries; the note carries the arithmetic.
691    Derived,
692    /// A seed for the metrics gym's calibration walk. Chosen, not established.
693    Provisional,
694}
695
696/// A named opening in the standard seam set, in cells.
697#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
698pub struct Opening {
699    /// Clear width, in cells.
700    pub width: u32,
701    /// Clear height, in cells.
702    pub height: u32,
703}
704
705/// A named stair-pitch standard: a rise:run pattern together with the vanilla
706/// blocks that realize it, and the per-step rise the realization actually
707/// presents to a walking body.
708#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
709pub struct Pitch {
710    /// Blocks of rise per `run` blocks of horizontal travel.
711    pub rise: u32,
712    /// Blocks of horizontal travel per `rise` blocks of rise.
713    pub run: u32,
714    /// The rise in sixteenths that one realized tread presents. A stair block
715    /// offers its lower half first, so a 1:1 stair run steps 8/16 twice per
716    /// block of rise rather than 16/16 once; a slab ramp steps 8/16 as well.
717    /// This is the number [`Metrics::self_check`] holds under the walk-up
718    /// budget, which is what makes "standard pitch" mean *walked*, not merely
719    /// *legal*.
720    pub step_16: i64,
721    /// The vanilla realization, named so the derivation above is checkable
722    /// against blocks rather than asserted.
723    pub realization: &'static str,
724}
725
726/// The datum convention: what a box's declared floor `y` names.
727#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
728pub struct Datum {
729    /// What a box's declared datum `y` names. `floor-surface`: the walk plane is
730    /// at `y`, and whatever stands in the box later puts its own floor there.
731    pub datum: &'static str,
732}
733
734/// One entry's value. `untagged`, so the export reads as the number or object it
735/// is rather than as a wrapper a consumer has to unpick.
736#[derive(Debug, Clone, PartialEq, Serialize)]
737#[serde(untagged)]
738pub enum MetricValue {
739    /// A count of cells, blocks or ticks.
740    Count(u32),
741    /// A measurement that is not a whole number of anything.
742    Number(f64),
743    /// A yes/no fact.
744    Flag(bool),
745    /// A named seam opening.
746    Opening(Opening),
747    /// A named stair pitch.
748    Pitch(Pitch),
749    /// The datum convention.
750    Datum(Datum),
751}
752
753/// One player metric: a fact of the pinned game.
754#[derive(Debug, Clone, PartialEq, Serialize)]
755pub struct PlayerEntry {
756    /// The value.
757    pub value: MetricValue,
758    /// What the value counts (`blocks`, `sixteenths`, `ticks`, …), or `none`.
759    pub unit: &'static str,
760    /// Where the number came from.
761    pub provenance: Provenance,
762    /// One sentence: the constant this **is**, the vanilla rule it states, or
763    /// the arithmetic it was derived by.
764    pub note: &'static str,
765}
766
767/// One building metric: a standard this project fixes.
768///
769/// The value is deliberately not a public field. [`BuildingEntry::value`] is the
770/// only way to read it and it takes `&mut `[`Reads`], so nothing can rest a
771/// verdict on an uncalibrated standard without saying that it did.
772#[derive(Debug, Clone, PartialEq, Serialize)]
773pub struct BuildingEntry {
774    /// The entry's own key, carried so a read records itself without the caller
775    /// having to repeat the name it looked up under.
776    key: &'static str,
777    value: MetricValue,
778    /// What the value counts, or `none`.
779    pub unit: &'static str,
780    /// Where the number came from. Orthogonal to `calibrated`: a pitch's
781    /// geometry can be derived from vanilla blocks while the judgement that it
782    /// is *the* standard is still unwalked.
783    pub provenance: Provenance,
784    /// Whether the metrics gym's walk has ruled on this value. `false` on every
785    /// entry at this version.
786    pub calibrated: bool,
787    /// One sentence: what the number is for and what the gym is being asked to
788    /// decide about it.
789    pub note: &'static str,
790}
791
792impl BuildingEntry {
793    /// Read the value, recording the read.
794    ///
795    /// The `&mut `[`Reads`] is the whole mechanism: [`Metrics::notice`] reports
796    /// exactly the uncalibrated entries a run actually consumed, so `DW0813`
797    /// cannot be forgotten by a check that reads one and can never fire over a
798    /// standard nothing looked at.
799    #[must_use]
800    pub fn value(&self, reads: &mut Reads) -> &MetricValue {
801        reads.record(self);
802        &self.value
803    }
804
805    /// The value with no read recorded — for rendering the table, never for
806    /// deciding anything.
807    ///
808    /// Reachable only inside this crate, and used at exactly one site: the
809    /// export, which reports the table rather than resting a verdict on it. A
810    /// serialization is not a verdict, and counting it as one would put every
811    /// entry in every `DW0813` line and make the code mean nothing.
812    pub(crate) fn value_for_display(&self) -> &MetricValue {
813        &self.value
814    }
815
816    /// The entry's key.
817    #[must_use]
818    pub fn key(&self) -> &'static str {
819        self.key
820    }
821}
822
823/// The building metrics a run's verdicts have read.
824///
825/// Deterministic (ADR-0006): a `BTreeSet` of `&'static str`, so the order a
826/// `DW0813` line names them in is the table's own order and not the order the
827/// checks happened to run in.
828#[derive(Debug, Clone, Default, PartialEq, Eq)]
829pub struct Reads {
830    read: BTreeSet<&'static str>,
831    provisional: BTreeSet<&'static str>,
832}
833
834impl Reads {
835    /// A ledger nothing has read through yet.
836    #[must_use]
837    pub fn new() -> Self {
838        Self::default()
839    }
840
841    fn record(&mut self, entry: &BuildingEntry) {
842        self.read.insert(entry.key);
843        if !entry.calibrated {
844            self.provisional.insert(entry.key);
845        }
846    }
847
848    /// How many building metrics this run's verdicts read, and how many of those
849    /// the gym has not walked.
850    #[must_use]
851    pub fn binding(&self) -> ReadBinding {
852        ReadBinding {
853            read: self.read.len(),
854            provisional: self.provisional.len(),
855        }
856    }
857
858    /// The uncalibrated entries read, in table order.
859    #[must_use]
860    pub fn provisional(&self) -> Vec<&'static str> {
861        self.provisional.iter().copied().collect()
862    }
863
864    /// Every entry read, in table order.
865    ///
866    /// The metrics gym's coverage numerator: an entry the gym's construction
867    /// never read is an entry no bay was built from, so a walk of the gym cannot
868    /// rule on it. Taking it from the same ledger the reads are recorded in is
869    /// what stops the gym's own coverage claim being a list somebody maintains
870    /// beside the generator.
871    #[must_use]
872    pub fn read(&self) -> BTreeSet<&'static str> {
873        self.read.clone()
874    }
875}
876
877/// What a run's building-metric reads bound to.
878#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
879pub struct ReadBinding {
880    /// Building metrics read.
881    pub read: usize,
882    /// Of those, entries whose `calibrated` is false.
883    pub provisional: usize,
884}
885
886/// A name a document wrote that this table does not define — `DW0812`'s payload.
887///
888/// It carries the defined set as well as the bad name, because the author's next
889/// action is choosing a real one and a refusal that only says *no* sends them to
890/// read the compiler.
891#[derive(Debug, Clone, PartialEq, Eq)]
892pub struct UnknownMetric {
893    /// The kind of entry the document was naming (`opening`, `stair pitch`, …).
894    pub kind: &'static str,
895    /// The same kind, plural — carried rather than derived, because three of the
896    /// six nouns end in a sibilant and `{kind}s` is wrong for them.
897    pub kind_plural: &'static str,
898    /// The name it wrote.
899    pub named: String,
900    /// Every name this table does define of that kind, in table order.
901    pub defined: Vec<&'static str>,
902}
903
904impl UnknownMetric {
905    /// The `DW0812` refusal, located by the caller.
906    #[must_use]
907    pub fn diagnostic(&self, stage: &str, path: &str) -> Diagnostic {
908        let defined = if self.defined.is_empty() {
909            "nothing".to_string()
910        } else {
911            self.defined.join(", ")
912        };
913        Diagnostic::error(
914            DW_METRIC_UNKNOWN,
915            stage,
916            path,
917            format!(
918                "the metrics table defines no {kind} called `{named}`. The table is \
919                 the single authority for this vocabulary, so a name it does not \
920                 define cannot compile and no check downstream has to cope with one. \
921                 Defined {plural}: {defined}. Run `delvec metrics` for the whole \
922                 table, including what each entry is for.",
923                kind = self.kind,
924                plural = self.kind_plural,
925                named = self.named,
926            ),
927        )
928    }
929}
930
931// ---------------------------------------------------------------------------
932// The table
933// ---------------------------------------------------------------------------
934
935/// The metrics standard, as exported and as read.
936#[derive(Debug, Clone, PartialEq, Serialize)]
937pub struct Metrics {
938    /// The table's revision.
939    pub metrics_version: u32,
940    /// The Minecraft version the player half states facts about.
941    pub mc_version: &'static str,
942    /// Facts of the pinned game, in key order.
943    pub player: BTreeMap<&'static str, PlayerEntry>,
944    /// Standards this project fixes, in key order.
945    pub building: BTreeMap<&'static str, BuildingEntry>,
946}
947
948/// Kinds of building entry a document can name, for [`Metrics::resolve`].
949#[derive(Debug, Clone, Copy, PartialEq, Eq)]
950pub enum MetricKind {
951    /// A seam opening (`opening.<name>`).
952    Opening,
953    /// A stair pitch (`pitch.<name>`).
954    Pitch,
955    /// A storey height (`storey.<name>`).
956    Storey,
957    /// A pacing coefficient (`pacing.<name>`) — blocks of route per minute of
958    /// play. Named like the rest of the vocabulary rather than reached by a
959    /// second lookup, because [`Metrics::resolve`] is the ONE path from a name
960    /// to an entry and a coefficient read any other way would be a second
961    /// authority (spec-0049 §2.2). No campaign document names one; the pacing
962    /// projection does.
963    Pacing,
964}
965
966impl MetricKind {
967    /// The key prefix entries of this kind carry.
968    #[must_use]
969    pub fn prefix(self) -> &'static str {
970        match self {
971            MetricKind::Opening => "opening.",
972            MetricKind::Pitch => "pitch.",
973            MetricKind::Storey => "storey.",
974            MetricKind::Pacing => "pacing.",
975        }
976    }
977
978    /// What the kind is called in a refusal, **plural**.
979    ///
980    /// A fact about the kind rather than an `s` appended where a message needed
981    /// one: `{noun}s` reads `stair pitchs`. Written out here, a kind
982    /// added later cannot inherit that by default — it has to answer.
983    #[must_use]
984    pub fn plural(self) -> &'static str {
985        match self {
986            MetricKind::Opening => "seam openings",
987            MetricKind::Pitch => "stair pitches",
988            MetricKind::Storey => "storey heights",
989            MetricKind::Pacing => "pacing coefficients",
990        }
991    }
992
993    /// What the kind is called in a refusal.
994    #[must_use]
995    pub fn noun(self) -> &'static str {
996        match self {
997            MetricKind::Opening => "seam opening",
998            MetricKind::Pitch => "stair pitch",
999            MetricKind::Storey => "storey height",
1000            MetricKind::Pacing => "pacing coefficient",
1001        }
1002    }
1003}
1004
1005fn player(
1006    value: MetricValue,
1007    unit: &'static str,
1008    provenance: Provenance,
1009    note: &'static str,
1010) -> PlayerEntry {
1011    PlayerEntry {
1012        value,
1013        unit,
1014        provenance,
1015        note,
1016    }
1017}
1018
1019fn building(
1020    key: &'static str,
1021    value: MetricValue,
1022    unit: &'static str,
1023    provenance: Provenance,
1024    note: &'static str,
1025) -> (&'static str, BuildingEntry) {
1026    (
1027        key,
1028        BuildingEntry {
1029            key,
1030            value,
1031            unit,
1032            provenance,
1033            // Every entry lands uncalibrated. The gym walk flips it, one entry
1034            // at a time, and that walk is the only thing that may.
1035            calibrated: false,
1036            note,
1037        },
1038    )
1039}
1040
1041impl Metrics {
1042    /// The table.
1043    ///
1044    /// Rebuilt per call rather than held in a `static`, because the values are a
1045    /// handful of maps and a `static` would need interior mutability the moment
1046    /// the gym's walk starts flipping `calibrated`. Deterministic by
1047    /// construction: `BTreeMap`, no clock, no environment.
1048    #[must_use]
1049    pub fn table() -> Self {
1050        let player_entries: Vec<(&'static str, PlayerEntry)> = vec![
1051            (
1052                "body.width",
1053                player(
1054                    MetricValue::Number(PLAYER_WIDTH),
1055                    "blocks",
1056                    Provenance::EngineConstant,
1057                    "The standing collision box is 0.6 wide; `dsl::metrics::PLAYER_WIDTH` \
1058                     is the definition `compiler::nav`'s entity dims table and \
1059                     `compiler::crosshair` both import.",
1060                ),
1061            ),
1062            (
1063                "body.height",
1064                player(
1065                    MetricValue::Number(PLAYER_HEIGHT),
1066                    "blocks",
1067                    Provenance::EngineConstant,
1068                    "The standing collision box is 1.8 tall; the same definition the \
1069                     entity dims table reads for `minecraft:player`.",
1070                ),
1071            ),
1072            (
1073                "body.crouched-height",
1074                player(
1075                    MetricValue::Number(PLAYER_CROUCHED_HEIGHT),
1076                    "blocks",
1077                    Provenance::VanillaRule,
1078                    "A sneaking player's collision box is 1.5 tall, which is what lets a \
1079                     body pass under a cell a standing one cannot. No engine constant \
1080                     held this before the table, and nothing in this workspace has \
1081                     measured it on a running server.",
1082                ),
1083            ),
1084            (
1085                "body.eye-height",
1086                player(
1087                    MetricValue::Number(PLAYER_EYE_HEIGHT),
1088                    "blocks",
1089                    Provenance::EngineConstant,
1090                    "The eye sits 1.62 above the floor of the cell the body stands in. \
1091                     This is the definition the render plan, the viewer page and the \
1092                     creator overlay all import, having each declared their own before.",
1093                ),
1094            ),
1095            (
1096                "body.max-health",
1097                player(
1098                    MetricValue::Number(PLAYER_MAX_HEALTH),
1099                    "half-hearts",
1100                    Provenance::EngineConstant,
1101                    "Twenty points of health, the definition `compiler::combat` imports \
1102                     for its winnability arithmetic.",
1103                ),
1104            ),
1105            (
1106                "step.walk-up",
1107                player(
1108                    MetricValue::Count(MAX_AUTO_STEP_16 as u32),
1109                    "sixteenths",
1110                    Provenance::EngineConstant,
1111                    "The largest rise a walker crosses without jumping: vanilla's player \
1112                     `maxUpStep` is 0.6 blocks and 9/16 is the largest sixteenth under \
1113                     it. `compiler::nav` imports this as its step rule's walk-up budget.",
1114                ),
1115            ),
1116            (
1117                "step.jump-rise",
1118                player(
1119                    MetricValue::Count(MAX_JUMP_RISE_16 as u32),
1120                    "sixteenths",
1121                    Provenance::EngineConstant,
1122                    "The largest rise a walker reaches by jumping: the apex is ≈1.2522 \
1123                     blocks, so 20/16 is reachable and 21/16 is not. `compiler::nav` \
1124                     imports this, and it is why a slab-to-full-block step-up of 1.5 is \
1125                     the impossible move it is.",
1126                ),
1127            ),
1128            (
1129                "step.block",
1130                player(
1131                    MetricValue::Count(FULL_16 as u32),
1132                    "sixteenths",
1133                    Provenance::EngineConstant,
1134                    "A full block in the sixteenths the step rule is denominated in, so \
1135                     that no comparison in the navigation model is a float.",
1136                ),
1137            ),
1138            (
1139                "jump.airborne",
1140                player(
1141                    MetricValue::Number(JUMP_AIRBORNE_TICKS),
1142                    "ticks",
1143                    Provenance::VanillaRule,
1144                    "A jump is airborne about twelve ticks, apex to landing. This was \
1145                     prose in the navigation model's elevation-weight derivation and \
1146                     nothing could read it; the table is where it becomes data.",
1147                ),
1148            ),
1149            (
1150                "walk.speed",
1151                player(
1152                    MetricValue::Number(WALK_SPEED_BLOCKS_PER_SECOND),
1153                    "blocks/second",
1154                    Provenance::VanillaRule,
1155                    "A walking player covers 4.317 blocks a second on the flat; \
1156                     sprinting is faster and is not the pacing basis, because a route \
1157                     nobody has learnt is walked.",
1158                ),
1159            ),
1160            (
1161                "sprint.speed",
1162                player(
1163                    MetricValue::Number(SPRINT_SPEED_BLOCKS_PER_SECOND),
1164                    "blocks/second",
1165                    Provenance::VanillaRule,
1166                    "A sprinting player covers 5.612 blocks a second on the flat (the \
1167                     wiki's Sprinting page). Not a pacing basis; it is how far a body can \
1168                     carry itself under a darkness grant, which leaves the sprint, in the \
1169                     blind-reach proof.",
1170                ),
1171            ),
1172            (
1173                "walk.ticks-per-block",
1174                player(
1175                    MetricValue::Number(walk_ticks_per_block()),
1176                    "ticks",
1177                    Provenance::Derived,
1178                    "Twenty ticks a second over 4.317 blocks a second. Against the \
1179                     twelve airborne ticks of a jump this is what makes a block of \
1180                     climb cost about two and a half blocks of walking, which is where \
1181                     the navigation model's elevation weight of two comes from.",
1182                ),
1183            ),
1184            (
1185                "fluid.passable",
1186                player(
1187                    MetricValue::Flag(false),
1188                    "none",
1189                    Provenance::VanillaRule,
1190                    "Water and lava are impassable to every proof in this engine and are \
1191                     never floor either: a body cannot stand on a fluid surface, so the \
1192                     two sets are disjoint and both gate standability. Marked as a rule \
1193                     rather than an engine constant deliberately — the navigation model \
1194                     encodes this as the SHAPE of its occupancy sets and not as a shared \
1195                     `const`, so there is no single definition for this row to be, and \
1196                     claiming otherwise would be the overclaim the provenance field \
1197                     exists to prevent.",
1198                ),
1199            ),
1200            (
1201                "fall.damage-onset",
1202                player(
1203                    MetricValue::Number(FALL_DAMAGE_ONSET_BLOCKS),
1204                    "blocks",
1205                    Provenance::VanillaRule,
1206                    "Fall damage is `ceil(distance − 3)` points, so a three-block fall is \
1207                     free and a four-block fall costs one. Stated from the vanilla \
1208                     damage rule; this repository has not measured it on a running \
1209                     server.",
1210                ),
1211            ),
1212            (
1213                "fall.unarmoured-survivable",
1214                player(
1215                    MetricValue::Number(unarmoured_survivable_fall_blocks()),
1216                    "blocks",
1217                    Provenance::Derived,
1218                    "`ceil(distance − 3) < 20` holds up to 22 blocks, which lands a \
1219                     full-health unarmoured body on one half-heart; 23 deals twenty and \
1220                     kills. The survivable ceiling is a function of health and armour, \
1221                     and this is its unarmoured, full-health case — the physical bound \
1222                     no designed drop may pass.",
1223                ),
1224            ),
1225            (
1226                "passable.width",
1227                player(
1228                    MetricValue::Count(passable_width_cells()),
1229                    "cells",
1230                    Provenance::Derived,
1231                    "`ceil(0.6)`: one cell is the narrowest a standing body fits \
1232                     through. This is a fact, not a standard — no walk can change it.",
1233                ),
1234            ),
1235            (
1236                "passable.clearance",
1237                player(
1238                    MetricValue::Count(passable_clearance_cells()),
1239                    "cells",
1240                    Provenance::Derived,
1241                    "`ceil(1.8)`: two cells is the lowest a standing body passes under. \
1242                     Like the width beside it this is a fact rather than a standard, \
1243                     which is why neither carries a calibration flag.",
1244                ),
1245            ),
1246        ];
1247
1248        let building_entries: Vec<(&'static str, BuildingEntry)> = vec![
1249            building(
1250                "datum",
1251                MetricValue::Datum(Datum {
1252                    datum: "floor-surface",
1253                }),
1254                "none",
1255                Provenance::Provisional,
1256                "The datum convention: a box's floor SURFACE is at its declared y, and \
1257                 whatever stands in the box later puts its walk plane there. A box's \
1258                 footprint is any whole number of blocks on either axis.",
1259            ),
1260            building(
1261                "opening.door",
1262                MetricValue::Opening(Opening {
1263                    width: 1,
1264                    height: 2,
1265                }),
1266                "cells",
1267                Provenance::Provisional,
1268                "The narrow seam: one body at a time, the size of a vanilla door. Seeded \
1269                 at the smallest opening a standing body passes, so the gym is deciding \
1270                 whether the tightest legal seam is one anybody wants to walk.",
1271            ),
1272            building(
1273                "opening.arch",
1274                MetricValue::Opening(Opening {
1275                    width: 2,
1276                    height: 3,
1277                }),
1278                "cells",
1279                Provenance::Provisional,
1280                "The ordinary seam between two interior places: two abreast, headroom \
1281                 over both.",
1282            ),
1283            building(
1284                "opening.passage",
1285                MetricValue::Opening(Opening {
1286                    width: 3,
1287                    height: 3,
1288                }),
1289                "cells",
1290                Provenance::Derived,
1291                "The three-by-three doorway the existing jigsaw socket conventions \
1292                 already standardize on — `cave:socket` and `tk:socket` are both this \
1293                 opening, so the prefab library has been built against it for as long \
1294                 as it has existed. Derived from that convention rather than chosen \
1295                 here, and still uncalibrated: what the gym decides is whether the \
1296                 convention is right, not what it is.",
1297            ),
1298            building(
1299                "opening.gateway",
1300                MetricValue::Opening(Opening {
1301                    width: 5,
1302                    height: 5,
1303                }),
1304                "cells",
1305                Provenance::Provisional,
1306                "The broad seam a thing of scenery scale passes: a cart, a barge, a \
1307                 processional. Seeded wide enough to read as an event from inside the \
1308                 place it opens onto, which is the judgement the walk is for.",
1309            ),
1310            building(
1311                "pitch.stair",
1312                MetricValue::Pitch(Pitch {
1313                    rise: 1,
1314                    run: 1,
1315                    step_16: 8,
1316                    realization: "minecraft:*_stairs",
1317                }),
1318                "none",
1319                Provenance::Derived,
1320                "One block of rise per block of run, realized in stair blocks. The \
1321                 geometry is vanilla's: a stair offers its lower half first, so the \
1322                 body walks two eight-sixteenth steps per block of rise and never \
1323                 jumps. What is uncalibrated is the comfort judgement — whether a climb \
1324                 this steep is one a player wants to make repeatedly.",
1325            ),
1326            building(
1327                "pitch.ramp",
1328                MetricValue::Pitch(Pitch {
1329                    rise: 1,
1330                    run: 2,
1331                    step_16: 8,
1332                    realization: "minecraft:*_slab + full block",
1333                }),
1334                "none",
1335                Provenance::Derived,
1336                "One block of rise per two of run, realized as a bottom slab then a full \
1337                 block. Same eight-sixteenth tread as the stair and half the pitch, so \
1338                 it is the gentle standard; the run it costs is what the gym weighs it \
1339                 on.",
1340            ),
1341            building(
1342                "storey.low",
1343                MetricValue::Count(5),
1344                "blocks",
1345                Provenance::Derived,
1346                "Floor course, three cells of interior clearance, ceiling course. Taken \
1347                 from the existing cave tileset, every passage and room of which is five \
1348                 blocks tall, so this is the storey the shipped library already has \
1349                 rather than a number invented here.",
1350            ),
1351            building(
1352                "storey.standard",
1353                MetricValue::Count(8),
1354                "blocks",
1355                Provenance::Provisional,
1356                "The storey an interior room of consequence gets: six cells of clearance \
1357                 between courses. A seed, and the walk is what says whether a room this \
1358                 tall reads as generous or merely as far away.",
1359            ),
1360            building(
1361                "storey.hall",
1362                MetricValue::Count(14),
1363                "blocks",
1364                Provenance::Provisional,
1365                "The storey a hall gets, where the height itself is the effect. The seed \
1366                 is deliberately at the point where volume starts costing walking time \
1367                 for nothing, because that is the trade the walk has to judge.",
1368            ),
1369            building(
1370                "pacing.route-blocks-per-minute",
1371                MetricValue::Count(60),
1372                "blocks/minute",
1373                Provenance::Provisional,
1374                "Blocks of route a party gets through per minute of play, once looking, \
1375                 fighting and backtracking are in it. Carried with NO THRESHOLD \
1376                 anywhere until the first walked blockout and the first full playtest \
1377                 calibrate it: a threshold on a number this uncertain would be defending \
1378                 nothing. Its upper bound is the pure-walk figure beside it, which no \
1379                 party achieves.",
1380            ),
1381            building(
1382                "pacing.walk-only-blocks-per-minute",
1383                MetricValue::Count((WALK_SPEED_BLOCKS_PER_SECOND * 60.0) as u32),
1384                "blocks/minute",
1385                Provenance::Derived,
1386                "Walking speed times sixty: what a body covers doing nothing but \
1387                 walking in a straight line. It exists so the route coefficient above \
1388                 has a ceiling that is a fact rather than another guess, and so the \
1389                 ratio between them is the thing the playtest actually measures.",
1390            ),
1391        ];
1392
1393        Metrics {
1394            metrics_version: METRICS_VERSION,
1395            mc_version: crate::blocks::MC_VERSION,
1396            player: player_entries.into_iter().collect(),
1397            building: building_entries.into_iter().collect(),
1398        }
1399    }
1400
1401    /// Resolve a name a **document** wrote to its building entry.
1402    ///
1403    /// This is the **only** path from an authored name to an entry, and it is
1404    /// what makes the table the single authority rather than a suggestion: a
1405    /// name it does not define cannot be resolved, so it cannot compile, so no
1406    /// check downstream ever meets one.
1407    ///
1408    /// The other half of that guarantee is that no key string is spelled outside
1409    /// this module. The entry no document names — the datum convention — is
1410    /// reached through the accessors below rather than by looking
1411    /// the key up in [`Metrics::building`], which is public so that a *reporter*
1412    /// can walk the whole table (`delvec metrics` counts it; the tests iterate
1413    /// it). Reporting is not resolution: a caller that walks every entry cannot
1414    /// name a wrong one, and a caller that wants ONE entry has an accessor and
1415    /// therefore no reason to type a key.
1416    ///
1417    /// # Errors
1418    ///
1419    /// [`UnknownMetric`], which the caller turns into `DW0812` with its own
1420    /// stage and path.
1421    pub fn resolve(&self, kind: MetricKind, named: &str) -> Result<&BuildingEntry, UnknownMetric> {
1422        let key = format!("{}{}", kind.prefix(), named);
1423        self.building
1424            .get(key.as_str())
1425            .ok_or_else(|| UnknownMetric {
1426                kind: kind.noun(),
1427                kind_plural: kind.plural(),
1428                named: named.to_string(),
1429                defined: self.names_of(kind),
1430            })
1431    }
1432
1433    /// The datum convention that fixes what a declared floor `y` means.
1434    ///
1435    /// One of the entries **no document names**: an author writes a number, not
1436    /// the word `datum`, so it has no place in [`Metrics::resolve`]'s naming
1437    /// vocabulary and would need a [`MetricKind`] whose prefix is the empty
1438    /// string — which would make `names_of` return the whole table. An accessor
1439    /// instead, so the key string still lives here and nowhere else.
1440    ///
1441    /// `None` only if the table stopped defining it, which
1442    /// [`Metrics::self_check`] reports as an internal error.
1443    #[must_use]
1444    pub fn datum(&self, reads: &mut Reads) -> Option<Datum> {
1445        match self.building.get("datum")?.value(reads) {
1446            MetricValue::Datum(g) => Some(*g),
1447            _ => None,
1448        }
1449    }
1450
1451    /// Every name defined for a kind, in table order.
1452    #[must_use]
1453    pub fn names_of(&self, kind: MetricKind) -> Vec<&'static str> {
1454        self.building
1455            .keys()
1456            .filter_map(|k| k.strip_prefix(kind.prefix()))
1457            .collect()
1458    }
1459
1460    /// The `DW0813` notice for a run, or `None` when no verdict rested on an
1461    /// unwalked standard.
1462    ///
1463    /// `None` at a zero binding is the calibrated end state and not a vacuity:
1464    /// the line's job is to say that a green rests on a seed, and once the gym
1465    /// has walked every entry a run reads there is nothing left for it to say.
1466    /// The distinguishable failure — a run that read NOTHING — is reported by
1467    /// [`Reads::binding`], which every caller states whether or not this returns
1468    /// a line.
1469    #[must_use]
1470    pub fn notice(&self, reads: &Reads, stage: &str) -> Option<Diagnostic> {
1471        let provisional = reads.provisional();
1472        if provisional.is_empty() {
1473            return None;
1474        }
1475        let binding = reads.binding();
1476        Some(Diagnostic::warning(
1477            DW_METRIC_PROVISIONAL,
1478            stage,
1479            "",
1480            format!(
1481                "{n} of the {read} building metric(s) this run read are provisional — the \
1482                 metrics gym has not walked them: {names}. The checks still ran and still \
1483                 refuse; what is unproven is the number they refused against.",
1484                n = binding.provisional,
1485                read = binding.read,
1486                names = provisional.join(", "),
1487            ),
1488        ))
1489    }
1490
1491    /// Check the table against itself and against the player half.
1492    ///
1493    /// These are verdicts, and they read building metrics through a [`Reads`], so
1494    /// they are also what gives `DW0813` a live binding at this version: `delvec
1495    /// metrics` runs them, and the notice names the seeds the consistency verdict
1496    /// rested on. They are the reason the mechanism is demonstrable now rather
1497    /// than at the round that adds the documents.
1498    ///
1499    /// A violation is an **internal error**, not a diagnostic: the table is
1500    /// engine data, so an inconsistent one is a defect in this file and not in
1501    /// anybody's campaign, and there is no author to address a refusal to.
1502    #[must_use]
1503    pub fn self_check(&self) -> SelfCheck {
1504        let mut reads = Reads::new();
1505        let mut failures: Vec<String> = Vec::new();
1506        let mut checked = 0usize;
1507
1508        let floor_w = u64::from(passable_width_cells());
1509        let floor_h = u64::from(passable_clearance_cells());
1510
1511        if self.datum(&mut reads).is_some() {
1512            checked += 1;
1513        } else {
1514            failures.push("the table defines no `datum` convention".to_string());
1515        }
1516
1517        for (key, entry) in &self.building {
1518            match entry.value(&mut reads) {
1519                MetricValue::Opening(o) => {
1520                    checked += 1;
1521                    if u64::from(o.width) < floor_w || u64::from(o.height) < floor_h {
1522                        failures.push(format!(
1523                            "`{key}` is {}×{}, which no standing body passes ({floor_w}×{floor_h} \
1524                             is the floor)",
1525                            o.width, o.height
1526                        ));
1527                    }
1528                }
1529                MetricValue::Pitch(p) => {
1530                    checked += 1;
1531                    if p.step_16 > MAX_AUTO_STEP_16 {
1532                        failures.push(format!(
1533                            "`{key}` presents a tread of {}/16, over the {MAX_AUTO_STEP_16}/16 \
1534                             walk-up budget, so it is climbed by jumping and is not a standard \
1535                             pitch",
1536                            p.step_16
1537                        ));
1538                    }
1539                    if p.rise == 0 || p.run == 0 {
1540                        failures.push(format!("`{key}` has a zero rise or run"));
1541                    }
1542                }
1543                _ => {}
1544            }
1545        }
1546
1547        for (key, floor) in [
1548            ("storey.low", floor_h + 2),
1549            ("storey.standard", floor_h + 2),
1550            ("storey.hall", floor_h + 2),
1551        ] {
1552            let Some(entry) = self.building.get(key) else {
1553                failures.push(format!("the table defines no `{key}`"));
1554                continue;
1555            };
1556            checked += 1;
1557            if let MetricValue::Count(n) = entry.value(&mut reads)
1558                && u64::from(*n) < floor
1559            {
1560                failures.push(format!(
1561                    "`{key}` is {n} blocks, which leaves no passable interior between a \
1562                     floor course and a ceiling course ({floor} is the floor)"
1563                ));
1564            }
1565        }
1566
1567        // A party cannot out-pace a body walking in a straight line.
1568        if let (Some(route), Some(walk)) = (
1569            self.building.get("pacing.route-blocks-per-minute"),
1570            self.building.get("pacing.walk-only-blocks-per-minute"),
1571        ) {
1572            checked += 1;
1573            if let (MetricValue::Count(r), MetricValue::Count(w)) =
1574                (route.value(&mut reads), walk.value(&mut reads))
1575                && r > w
1576            {
1577                failures.push(format!(
1578                    "`pacing.route-blocks-per-minute` is {r}, over the pure-walk ceiling of {w}"
1579                ));
1580            }
1581        }
1582
1583        SelfCheck {
1584            binding: SelfCheckBinding {
1585                invariants: checked,
1586                entries: self.building.len(),
1587                reads: reads.binding(),
1588            },
1589            reads,
1590            failures,
1591        }
1592    }
1593}
1594
1595/// What [`Metrics::self_check`] examined and what it found.
1596#[derive(Debug, Clone, PartialEq)]
1597pub struct SelfCheck {
1598    /// The ledger the verdicts read through, for [`Metrics::notice`].
1599    pub reads: Reads,
1600    /// What the run bound to. Stated whether or not anything failed, because a
1601    /// check that examined nothing is a finding and not a pass.
1602    pub binding: SelfCheckBinding,
1603    /// Inconsistencies, each a whole sentence. Non-empty is an internal error.
1604    pub failures: Vec<String>,
1605}
1606
1607/// [`Metrics::self_check`]'s binding count.
1608#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1609pub struct SelfCheckBinding {
1610    /// Invariants evaluated.
1611    pub invariants: usize,
1612    /// Building entries in the table.
1613    pub entries: usize,
1614    /// What those invariants read.
1615    pub reads: ReadBinding,
1616}
1617
1618/// The table rendered for export: every entry with its value, unit, provenance
1619/// and note, plus the halves' own counts.
1620///
1621/// Separate from [`Metrics`]'s own `Serialize` because the building half's value
1622/// is private — an export is the one consumer that reads a number without
1623/// resting a verdict on it.
1624#[must_use]
1625pub fn export(metrics: &Metrics) -> serde_json::Value {
1626    let mut player = serde_json::Map::new();
1627    for (k, e) in &metrics.player {
1628        player.insert((*k).to_string(), serde_json::json!(e));
1629    }
1630    let mut building = serde_json::Map::new();
1631    let mut uncalibrated = 0usize;
1632    for (k, e) in &metrics.building {
1633        if !e.calibrated {
1634            uncalibrated += 1;
1635        }
1636        building.insert(
1637            (*k).to_string(),
1638            serde_json::json!({
1639                "value": e.value_for_display(),
1640                "unit": e.unit,
1641                "provenance": e.provenance,
1642                "calibrated": e.calibrated,
1643                "note": e.note,
1644            }),
1645        );
1646    }
1647    serde_json::json!({
1648        "metrics_version": metrics.metrics_version,
1649        "mc_version": metrics.mc_version,
1650        "counts": {
1651            "player": metrics.player.len(),
1652            "building": metrics.building.len(),
1653            "uncalibrated": uncalibrated,
1654        },
1655        "player": player,
1656        "building": building,
1657    })
1658}
1659
1660#[cfg(test)]
1661mod tests {
1662    use super::*;
1663
1664    #[test]
1665    fn the_table_is_consistent_with_itself_and_with_the_player_half() {
1666        let m = Metrics::table();
1667        let check = m.self_check();
1668        assert!(
1669            check.failures.is_empty(),
1670            "the shipped metrics table is inconsistent: {:?}",
1671            check.failures
1672        );
1673        assert!(
1674            check.binding.invariants > 0,
1675            "a self-check that evaluated nothing is vacuous, not a pass"
1676        );
1677    }
1678
1679    #[test]
1680    fn every_building_entry_lands_uncalibrated() {
1681        let m = Metrics::table();
1682        assert!(!m.building.is_empty(), "the building half is empty");
1683        for (k, e) in &m.building {
1684            assert!(
1685                !e.calibrated,
1686                "`{k}` claims to be calibrated, but the metrics gym has not been walked"
1687            );
1688        }
1689    }
1690
1691    #[test]
1692    fn a_verdict_that_reads_a_seed_raises_dw0813_naming_it() {
1693        let m = Metrics::table();
1694        let check = m.self_check();
1695        assert!(
1696            check.binding.reads.provisional > 0,
1697            "the self-check read no provisional entry, so DW0813 binds to nothing"
1698        );
1699        let d = m
1700            .notice(&check.reads, "metrics")
1701            .expect("a run that read a seed owes the notice");
1702        assert_eq!(d.code, "DW0813");
1703        assert_eq!(d.severity, crate::diagnostic::Severity::Warning);
1704        for name in check.reads.provisional() {
1705            assert!(
1706                d.message.contains(name),
1707                "the notice must name `{name}`, the seed a verdict rested on"
1708            );
1709        }
1710    }
1711
1712    #[test]
1713    fn a_run_that_read_nothing_provisional_gets_no_notice() {
1714        let m = Metrics::table();
1715        let reads = Reads::new();
1716        assert!(m.notice(&reads, "metrics").is_none());
1717        assert_eq!(reads.binding().read, 0);
1718    }
1719
1720    #[test]
1721    fn an_undefined_name_is_dw0812_and_names_what_is_defined() {
1722        let m = Metrics::table();
1723        let err = m
1724            .resolve(MetricKind::Opening, "cathedral")
1725            .expect_err("`cathedral` is not an opening");
1726        let d = err.diagnostic("site-plan", "/content/seams/0/opening");
1727        assert_eq!(d.code, "DW0812");
1728        assert!(d.message.contains("cathedral"));
1729        assert!(d.message.contains("arch"), "the defined set is named");
1730        assert_eq!(d.stage, "site-plan");
1731        assert_eq!(d.path, "/content/seams/0/opening");
1732    }
1733
1734    #[test]
1735    fn every_kind_resolves_at_least_one_defined_name() {
1736        let m = Metrics::table();
1737        for kind in [MetricKind::Opening, MetricKind::Pitch, MetricKind::Storey] {
1738            let names = m.names_of(kind);
1739            assert!(
1740                !names.is_empty(),
1741                "{} resolves nothing, so DW0812 would refuse every name",
1742                kind.noun()
1743            );
1744            for n in names {
1745                assert!(m.resolve(kind, n).is_ok(), "`{n}` does not resolve");
1746            }
1747        }
1748    }
1749
1750    // --- a body has a width -------------------------------------------------
1751
1752    /// The fact the whole rule rests on, written down where it can be checked: a
1753    /// box selector spelled `x=lo,dx=hi-lo` is the world volume `[lo, hi+1]`, so
1754    /// `dx=0` is one whole block and never a plane.
1755    #[test]
1756    fn a_box_selector_spans_the_whole_of_its_last_cell() {
1757        assert_eq!(
1758            selection_aabb([7, 63, 20], [11, 67, 24]),
1759            ([7.0, 63.0, 20.0], [12.0, 68.0, 25.0])
1760        );
1761        let (lo, hi) = selection_aabb([0, 0, 0], [0, 0, 0]);
1762        assert_eq!((lo, hi), ([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]));
1763    }
1764
1765    /// **The measurement this rule was written from.** A wave's spider, 1.4 wide,
1766    /// walked to `x = 12.59` beside a pit whose box ends at `x = 12.0` and was
1767    /// killed there — its hitbox reaches `11.89`. Its feet cell is 12, which is
1768    /// outside the box, so every cell-shaped reading in the engine called that
1769    /// position safe.
1770    #[test]
1771    fn a_body_beside_a_face_is_inside_the_volume_and_its_cell_is_not() {
1772        let west_pit = ([7, 63, 20], [11, 67, 24]);
1773        let spider = Body::new(1.4, 1.4);
1774        assert!(
1775            body_meets_volume([12.59, 65.0, 21.02], spider, west_pit.0, west_pit.1),
1776            "hitbox 11.89..13.29 against a box face at 12.0"
1777        );
1778        // The cell it stands in is not one of the volume's cells.
1779        assert!(!(west_pit.0[0]..=west_pit.1[0]).contains(&12));
1780        // A player at the same cell's CENTRE is clear of it — 12.2..12.8 — which
1781        // is why the seat rule and the walk rule are two different readings.
1782        assert!(!body_meets_volume(
1783            [12.5, 65.0, 21.5],
1784            Body::PLAYER,
1785            west_pit.0,
1786            west_pit.1
1787        ));
1788        // …and the walk rule refuses that cell anyway, because a walker does not
1789        // stand at cell centres.
1790        assert!(cell_can_meet_volume(
1791            [12, 65, 21],
1792            Body::PLAYER,
1793            west_pit.0,
1794            west_pit.1
1795        ));
1796    }
1797
1798    /// The touching case is NOT an intersection, exactly as vanilla's own
1799    /// `AABB::intersects` is strict: a body standing on a volume's top face with
1800    /// its feet at the face is clear of it.
1801    #[test]
1802    fn a_face_that_exactly_touches_is_not_inside() {
1803        let v = ([0, 0, 0], [0, 0, 0]);
1804        assert!(!body_meets_volume([0.5, 1.0, 0.5], Body::PLAYER, v.0, v.1));
1805        assert!(body_meets_volume([0.5, 0.999, 0.5], Body::PLAYER, v.0, v.1));
1806    }
1807
1808    /// The keep-out box is a DERIVATION, not a constant one: it widens with the
1809    /// body. Horizontally every body up to two blocks wide reaches exactly one
1810    /// cell (half a width can never cross two cell boundaries at once);
1811    /// vertically a body taller than two blocks reaches one cell further down
1812    /// than a player does, which is the only place the bodies in this engine's
1813    /// dims table differ.
1814    #[test]
1815    fn the_keep_out_box_widens_with_the_body() {
1816        let v = ([10, 60, 10], [12, 62, 12]);
1817        assert_eq!(
1818            keep_out_box(Body::PLAYER, v.0, v.1),
1819            ([9, 59, 9], [13, 62, 13])
1820        );
1821        // 1.4 wide: the same ring. Half a width is 0.7, which crosses one
1822        // boundary and not two.
1823        assert_eq!(
1824            keep_out_box(Body::new(1.4, 1.4), v.0, v.1),
1825            ([9, 59, 9], [13, 62, 13])
1826        );
1827        // 2.9 tall (a warden): one cell further DOWN, because its head reaches
1828        // into the volume from a cell a player's head does not.
1829        assert_eq!(
1830            keep_out_box(Body::new(0.9, 2.9), v.0, v.1),
1831            ([9, 58, 9], [13, 62, 13])
1832        );
1833        // Standing ON the volume's top cell is safe for every body: the feet
1834        // start where the volume ends.
1835        assert!(!cell_can_meet_volume(
1836            [11, 63, 11],
1837            Body::new(0.9, 2.9),
1838            v.0,
1839            v.1
1840        ));
1841    }
1842
1843    /// **Feet below the cell floor reach a volume in the course underneath.**
1844    /// At the cell floor [`feet_can_meet_volume`] is [`keep_out_box`] exactly,
1845    /// over every cell round a volume, for two bodies. A body standing on an
1846    /// upward dripstone tip (feet 11/16 into the tip's cell) meets a volume
1847    /// drawn in the tip course from the cell above it, which the cell-floor
1848    /// reading refuses; one standing on a full block over the volume does not.
1849    #[test]
1850    fn feet_below_the_floor_reach_the_course_they_stand_on() {
1851        let v = ([10, 60, 10], [12, 62, 12]);
1852        for body in [Body::PLAYER, Body::new(0.9, 2.9)] {
1853            let (klo, khi) = keep_out_box(body, v.0, v.1);
1854            for x in 7..=15 {
1855                for y in 55..=66 {
1856                    for z in 7..=15 {
1857                        let c = [x, y, z];
1858                        let boxed = (0..3).all(|i| klo[i] <= c[i] && c[i] <= khi[i]);
1859                        assert_eq!(
1860                            feet_can_meet_volume(c, i64::from(y) * 16, body, v.0, v.1),
1861                            boxed,
1862                            "{c:?}"
1863                        );
1864                    }
1865                }
1866            }
1867        }
1868        // The tip course is y = 62, the volume's top; the body stands in 63.
1869        let tip = [11, 63, 11];
1870        assert!(!cell_can_meet_volume(tip, Body::PLAYER, v.0, v.1));
1871        assert!(feet_can_meet_volume(
1872            tip,
1873            62 * 16 + 11,
1874            Body::PLAYER,
1875            v.0,
1876            v.1
1877        ));
1878        assert!(!feet_can_meet_volume(tip, 63 * 16, Body::PLAYER, v.0, v.1));
1879    }
1880
1881    #[test]
1882    fn the_derived_player_values_are_the_arithmetic_their_notes_claim() {
1883        assert!((walk_ticks_per_block() - 20.0 / 4.317).abs() < f64::EPSILON);
1884        assert!((walk_ticks_per_block() - 4.633).abs() < 0.001);
1885        assert_eq!(unarmoured_survivable_fall_blocks(), 22.0);
1886        assert_eq!(passable_width_cells(), 1);
1887        assert_eq!(passable_clearance_cells(), 2);
1888    }
1889
1890    #[test]
1891    fn the_jump_reach_runs_from_the_apex_to_the_deepest_survivable_fall_and_never_narrows() {
1892        assert_eq!(JUMP_REACH.first().map(|r| r.0), Some(1));
1893        assert_eq!(
1894            JUMP_REACH.last().map(|r| r.0),
1895            Some(-(unarmoured_survivable_fall_blocks() as i64))
1896        );
1897        for w in JUMP_REACH.windows(2) {
1898            assert_eq!(w[1].0, w[0].0 - 1, "one row per whole block of rise");
1899            assert!(
1900                w[1].1 >= w[0].1,
1901                "a deeper landing never admits a narrower gap: {w:?}"
1902            );
1903        }
1904        assert_eq!(jump_max_gap(MAX_JUMP_RISE_16), Some(3));
1905        assert_eq!(jump_max_gap(MAX_JUMP_RISE_16 + 1), None);
1906        assert_eq!(jump_max_gap(0), Some(4));
1907        assert_eq!(jump_max_gap(-24), Some(4), "a drop of 1.5 reads the -1 row");
1908        assert_eq!(jump_max_gap(-22 * FULL_16), Some(9));
1909        assert_eq!(jump_max_gap(-22 * FULL_16 - 1), None);
1910    }
1911}