feat(simulation): Layer-1 water bearing + D-234 waterfront rule (#957)

Close the last D-234 piece — terrain water-direction extraction wired through
to founding orientation and the quarter waterfront rule:

- Layer 1: TerrainAnalysis::water_bearing — 8-octant integer bearing toward the
  nearest water from the water_dist gradient (D-010, no atan2). Stored on
  GeographicAttractor.water_bearing (360 = none).
- #956 founding orientation: coastal/river settlements now get a real
  water-facing bearing (the anchoring attractor's), replacing the 0 stub.
- #957 waterfront rule (D-234b): the water-facing quarter edge (from the
  settlement's Coastal founding orientation) drops its block setback to 0 so
  buildings present flush to the quay (dock-orthogonal). Typed Edge + coastal_edge
  + per-block gating.

Golden + atlas_response fixture rebaked (additive water_bearing field only).
8 new tests. All integer-deterministic (D-010).

Pending: the waterfront rule reads context.founding_orientation, which
city_context_reader still stubs to Cardinal — real per-settlement orientation
reaches quarter generation once the Layer-3 placement -> Layer-4 GenerateSkeleton
dispatch is wired (the remaining cross-layer integration).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 10:22:14 +02:00
co-authored by Claude Opus 4.8
parent 4c6fe596d1
commit b73b4e6d92
8 changed files with 704 additions and 268 deletions
+64
View File
@@ -122,6 +122,70 @@ impl TerrainAnalysis {
pub fn is_ocean(&self, r: usize, c: usize) -> bool {
self.ocean_mask[idx(r, c, self.w)]
}
/// Compass bearing toward the nearest water from cell `(r, c)`, quantized to
/// 8 octants (0=N, 45=NE … 315=NW); `360` = "no water in range" (#957, D-234).
///
/// Reads the `water_dist` field's local gradient — the 8-neighbour with the
/// smallest distance-to-water points toward water. Integer-only (no `atan2`)
/// for D-010 determinism. Returns `360` when the cell is itself water or no
/// neighbour is closer to water (flat/inland).
pub fn water_bearing(&self, r: usize, c: usize) -> u16 {
let here = self.water_dist[idx(r, c, self.w)];
if here == 0 || here >= WATER_DIST_CAP {
return NO_WATER_BEARING; // on water, or no water within range
}
let mut best = here;
let mut bdir = (0i32, 0i32);
for &(dr, dc) in &NB8 {
let nr = r as i32 + dr;
if nr < 0 || nr >= self.h as i32 {
continue;
}
let nc = wrap_col(c as i32 + dc, self.w as i32);
let nd = self.water_dist[idx(nr as usize, nc, self.w)];
if nd < best {
best = nd;
bdir = (dr, dc);
}
}
if bdir == (0, 0) {
NO_WATER_BEARING
} else {
octant_bearing(bdir.0, bdir.1)
}
}
}
/// Sentinel for [`TerrainAnalysis::water_bearing`] meaning "no water direction".
pub const NO_WATER_BEARING: u16 = 360;
/// Quantize a (Δrow, Δcol) step to a compass octant bearing (0=N … 315=NW).
/// `Δrow < 0` is north (rows increase downward). Integer-only (D-010).
fn octant_bearing(drow: i32, dcol: i32) -> u16 {
let (ar, ac) = (drow.abs(), dcol.abs());
let north = drow < 0;
let east = dcol > 0;
if ar >= ac * 2 {
if north {
0
} else {
180
}
} else if ac >= ar * 2 {
if east {
90
} else {
270
}
} else {
match (north, east) {
(true, true) => 45,
(true, false) => 315,
(false, true) => 135,
(false, false) => 225,
}
}
}
/// Largest connected below-sea-level component = ocean; all others = lakes.