diff --git a/Tools/README.md b/Tools/README.md index 0608121..94c3b77 100644 --- a/Tools/README.md +++ b/Tools/README.md @@ -31,11 +31,12 @@ constants, carried over verbatim — not re-derived from a design summary** (→ | `Scripts/TerrainNoise.cs` | ⭐ The FastNoiseLite config, **every fractal property pinned explicitly** | | `Scripts/Topography.cs` | ⭐⭐ Pass 1 — the six elements, in the reference's execution order | | `Scripts/IslandFalloff.cs` | The whole reference file now: `SmoothAbs`, the **coast shelf**, the **offshore islets** + the reshape helpers | -| `Scripts/OffshorePass.cs` | ⭐ **Pass 1b** — shelf + islets over the finished pass-1 arrays, then `HMaxSeed` is retaken (chat2/05) | -| `Scripts/OffshoreSettings.cs` | Every islet dial in one object; `Faithful()` (the reference) and `Hybrid()` (the reshape) | -| `Scripts/OffshoreAnalysis.cs` | Island components, N/S counts, the moat check, **the hemisphere convention** | +| `Scripts/OffshorePass.cs` | ⭐ **Pass 1b** — shelf + the **organic islet layer** (the one island mechanism) + the slop guards, over the finished pass-1 arrays, then `HMaxSeed` is retaken (chat2/05, retuned chat2/06) | +| `Scripts/OffshoreSettings.cs` | Every islet dial in one object; `Faithful()` (the reference) and `Organic()` (the reshape, tuned: **density** + **south weight** + guards). ⚠ No floor / stamps / count guarantee — chat2/05's `Hybrid()` was reverted out in chat2/06 (git history has it) | +| `Scripts/OffshoreAnalysis.cs` | Island components, N/S counts, the moat check, the separation-guard geometry, **the hemisphere convention** | +| `Scripts/OffshoreDiagnosis.cs` | The per-hemisphere **measurement** — valid-zone area, binding gate, noise peaks over threshold (chat2/06 §2) | | `Scripts/TagOverlayRenderer.cs` | The island-tag / hemisphere debug overlay | -| `Scripts/OffshoreIslandsTool.cs` + `Scenes/OffshoreIslandsTool.tscn` | The chat2/05 batch | +| `Scripts/OffshoreIslandsTool.cs` + `Scenes/OffshoreIslandsTool.tscn` | The offshore batch — chat2/06: 4 plates + the count table + the diagnosis (the chat2/05 version is at `3b96e06`) | | `Scripts/Pass1Result.cs` | The height field **and the Phase-2 seams** | | `Scripts/TerrainGenConfig.cs` | Config + the per-element ablation toggles | | `Scripts/HeightField.cs` | Raw `.f32` save/load — **the generation/presentation seam** | @@ -207,8 +208,11 @@ loop with the submarine **coast shelf** and the **offshore islet** layer (~:621- second sweep over the finished arrays — `OffshorePass` — with the same per-pixel arithmetic in the same order, then **retakes `HMaxSeed` after them**, as the reference did (chat2/00 Drift §2, closed). The islets exist in two presets: `OffshoreSettings.Faithful()` (the reference, verbatim — the control) and -`OffshoreSettings.Hybrid()` (the reshape: small / low / flat / rigid, a seeded floor of ≥2 N / ≥4 S -islands whose positions vary per seed, organic extras weighted south, corners allowed). +`OffshoreSettings.Organic()` (the reshape: small / low / flat / crisp, corners allowed — **the organic +noise-field layer is the only island mechanism**, tuned for coverage by **density** and a **south +weight**, with speck / separation / blob guards; chat2/06). ⚠ **No forced count.** chat2/05's seeded +floor (`Hybrid()`: stamps guaranteeing ≥2 N / ≥4 S) was tried and **reverted out on look** in chat2/06; +the per-hemisphere counts are a statistical outcome of the tuning, read off the batch's count table. > ### ⚠ Both default OFF — deliberately, and that is a decision to revisit. > diff --git a/Tools/Scripts/IslandFalloff.cs b/Tools/Scripts/IslandFalloff.cs index e195084..0d4a802 100644 --- a/Tools/Scripts/IslandFalloff.cs +++ b/Tools/Scripts/IslandFalloff.cs @@ -10,7 +10,7 @@ namespace IslaApocalypse.Tools /// 2. the submarine COAST SHELF () — chat2/05 stage 1 /// 3. the OFFSHORE ISLET layer (, /// , ) — chat2/05 stage 1 - /// + the RESHAPE helpers (, ) — stage 2 + /// + the RESHAPE helper () — stage 2 /// /// The faithful functions keep the reference's names, constants and arithmetic verbatim (D-050); /// the parameterized overloads beside them exist so the reshape can move a dial without touching @@ -222,7 +222,8 @@ namespace IslaApocalypse.Tools } // ═══════════════════════════════════════════════════════════════════════ - // 3b. THE RESHAPE HELPERS — chat2/05 stage 2. Not in the reference. + // 3b. THE RESHAPE HELPER — chat2/05 stage 2. Not in the reference. (The seeded-floor + // stamp that sat beside it was reverted out in chat2/06 — git history has it.) // ═══════════════════════════════════════════════════════════════════════ /// @@ -238,20 +239,5 @@ namespace IslaApocalypse.Tools if (w <= 0f || edgeSharpness <= 1f) return w; return 1f - MathF.Pow(1f - w, edgeSharpness); } - - /// - /// The seeded-floor stamp: a flat disc of radius radius · coreFraction at full weight, - /// then a smoothstep rim down to zero at . Higher - /// ⇒ flatter top, crisper shore. This is the "rigid, defined - /// coastline" the reshape asks for, in its simplest honest form. - /// - public static float StampWeight(float dist, float radius, float coreFraction) - { - if (dist >= radius) return 0f; - float core = radius * coreFraction; - if (dist <= core) return 1f; - float t = (dist - core) / (radius - core); - return 1f - t * t * (3f - 2f * t); - } } } diff --git a/Tools/Scripts/OffshoreAnalysis.cs b/Tools/Scripts/OffshoreAnalysis.cs index 07be0e7..79e3de4 100644 --- a/Tools/Scripts/OffshoreAnalysis.cs +++ b/Tools/Scripts/OffshoreAnalysis.cs @@ -173,5 +173,64 @@ namespace IslaApocalypse.Tools foreach (var c in comps) if (c.BridgedToMainland) b++; return b; } + + // ═══ chat2/06 — the separation guard's geometry ═══ + + /// + /// Per component id, its BOUNDARY cells — tagged cells with at least one 8-neighbour that is + /// not tagged (or the map edge). The nearest approach between two islands is between + /// boundary cells, so the separation guard compares boundaries, not bodies: a few hundred + /// cells per island instead of thousands. is the pass's list of + /// every surfaced cell (the bodies), walked once. + /// + public static Dictionary> BoundaryCells(bool[,] tag, int[] compId, int mapSize, + IEnumerable<(int x, int y, float h0)> surfaced) + { + var result = new Dictionary>(); + foreach (var (x, y, _) in surfaced) + { + if (!tag[x, y]) continue; + int id = compId[x * mapSize + y]; + if (id == 0) continue; + bool edge = false; + for (int k = 0; k < 8 && !edge; k++) + { + int nx = x + DX[k], ny = y + DY[k]; + if (nx < 0 || nx >= mapSize || ny < 0 || ny >= mapSize || !tag[nx, ny]) edge = true; + } + if (!edge) continue; + if (!result.TryGetValue(id, out var list)) result[id] = list = new List<(int, int)>(); + list.Add((x, y)); + } + return result; + } + + /// Chebyshev gap between two components' bounding boxes (0 if they overlap). A lower bound on their true distance. + public static int BoxGap(IslandComponent a, IslandComponent b) + { + int gx = Math.Max(0, Math.Max(a.MinX - b.MaxX, b.MinX - a.MaxX)); + int gy = Math.Max(0, Math.Max(a.MinY - b.MaxY, b.MinY - a.MaxY)); + return Math.Max(gx, gy); + } + + /// + /// The minimum Chebyshev distance between two boundary sets, early-exiting once it is + /// known to be below (the caller only needs "closer than the + /// minimum or not"). + /// + public static int MinChebyshev(List<(int x, int y)> a, List<(int x, int y)> b, int below) + { + int best = int.MaxValue; + if (a == null || b == null) return best; + foreach (var (ax, ay) in a) + { + foreach (var (bx, by) in b) + { + int d = Math.Max(Math.Abs(ax - bx), Math.Abs(ay - by)); + if (d < best) { best = d; if (best < below) return best; } + } + } + return best; + } } } diff --git a/Tools/Scripts/OffshoreDiagnosis.cs b/Tools/Scripts/OffshoreDiagnosis.cs new file mode 100644 index 0000000..9d02197 --- /dev/null +++ b/Tools/Scripts/OffshoreDiagnosis.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Godot; +using IslaApocalypse.Core; + +namespace IslaApocalypse.Tools +{ + /// One hemisphere's share of the offshore zone, the gates, and the noise peaks. + public sealed class HemisphereDiagnosis + { + public string Name; + + // ---- the valid offshore zone ---- + public long Sea; // below-sea cells in this hemisphere (after the shelf) + public long Zone; // cells with zone weight > 0 (island-eligible) + public long ZoneFull; // cells with zone weight == 1 (clear of every feather) + + // ---- which gate blocks (non-exclusive: a cell may fail several) ---- + public long BlockDepth; // ambient depth < moat + public long BlockFalloff; // pre-Trench falloff < min (not "actually offshore") + public long BlockTrench; // at/past the outer bound + // ---- the SOLE blocker (a cell that fails exactly one gate — loosen that gate and it joins the zone) ---- + public long SoleDepth, SoleFalloff, SoleTrench; + + // ---- band geometry: per column, how many rows are ocean / zone in this hemisphere ---- + public double OceanRowsPerColumn, ZoneRowsPerColumn; + public int ColumnsWithNoZone; + + // ---- the noise peaks (strict 8-neighbour local maxima of the islet field, on sea cells) ---- + public int PeaksSea; // all maxima over sea + public int PeaksInZone; // maxima inside the zone — the CAPACITY for islands at this frequency + public int PeaksInZoneOverThr; // inside the zone AND clearing the threshold — the island candidates + public int PeaksOverThrLost; // clearing the threshold but OUTSIDE the zone — killed by a gate: + public int LostDepth, LostFalloff, LostTrench; // …which one(s) (non-exclusive) + + public double ZoneShareOfSea => Sea == 0 ? 0 : (double)Zone / Sea; + public double CandidatesPerMegacell => Zone == 0 ? 0 : PeaksInZoneOverThr * 1e6 / Zone; + } + + /// + /// ⭐ THE SOUTH-SUPPRESSION DIAGNOSIS (chat2/06 §2) — MEASURE, DON'T GUESS. Before a knob moves, + /// answer per hemisphere: how much island-eligible ocean is there, how many noise peaks clear the + /// threshold in it, and which gate is the binding one. Read-only: it evaluates the islet noise + /// field and the zone mask exactly as does (same noise factory, same + /// calibration samples, same blended threshold, same gate arithmetic) over the post-shelf, + /// pre-islet field, and counts. It raises nothing. + /// + /// Hemisphere is the row midline (), as the tag. + /// + public static class OffshoreDiagnosis + { + public sealed class Report + { + public int Seed, MapSize; + public float ThresholdNorth, ThresholdSouth; + public HemisphereDiagnosis North = new() { Name = "north" }; + public HemisphereDiagnosis South = new() { Name = "south" }; + public HemisphereDiagnosis Of(byte hemi) => hemi == OffshoreAnalysis.HemiNorth ? North : South; + } + + /// The post-shelf, pre-islet field (offshore OFF, shelf as the batch runs it). + public static Report Run(float[,] height, float[,] preTrench, int mapSize, int seed, float sea, + OffshoreSettings s, GenerationScale scale) + { + var rep = new Report { Seed = seed, MapSize = mapSize }; + FastNoiseLite noise = TerrainNoise.CreateModulation(seed, s.SeedOffset, s.FreqPerMapWidth, scale); + float[] samples = OffshorePass.CalibrationSamples(noise, mapSize); + float thrN = IslandFalloff.CalibrateThreshold(samples, s.Density); + float thrS = s.Mode == OffshoreMode.Faithful ? thrN : IslandFalloff.CalibrateThreshold(samples, s.DensitySouth); + rep.ThresholdNorth = thrN; rep.ThresholdSouth = thrS; + + float centerX = mapSize / 2.0f, centerY = mapSize / 2.0f, halfSpan = mapSize / 2.0f; + float mid = mapSize * 0.5f; + float band = MathF.Max(1f, s.HemisphereBlendHalfWidth * mapSize); + + // The islet field over the whole map (a peak's neighbours may be land or out of zone). + var v = new float[mapSize, mapSize]; + for (int x = 0; x < mapSize; x++) + for (int y = 0; y < mapSize; y++) + v[x, y] = (noise.GetNoise2D(x, y) + 1f) * 0.5f; + + // Zone weight per cell, and the gate ledger. -1 = land. + var zone = new float[mapSize, mapSize]; + int half = mapSize / 2; + long[] oceanRows = new long[2], zoneRows = new long[2]; + int[] colsNoZone = new int[2]; + for (int x = 0; x < mapSize; x++) + { + long[] colZone = new long[2]; + for (int y = 0; y < mapSize; y++) + { + float h = height[x, y]; + if (h >= sea) { zone[x, y] = -1f; continue; } + int hi = y < half ? 0 : 1; + var d = hi == 0 ? rep.North : rep.South; + d.Sea++; oceanRows[hi]++; + + float depthM = WorldScale.MetresFromRaw(sea - h); + float dist = MathF.Max(MathF.Abs(x - centerX) / halfSpan, MathF.Abs(y - centerY) / halfSpan); + bool bDepth = depthM < s.MinDepthM; + bool bFall = preTrench[x, y] < s.MinFalloff; + bool bTr = dist >= s.TrenchOuter; + if (bDepth) d.BlockDepth++; + if (bFall) d.BlockFalloff++; + if (bTr) d.BlockTrench++; + int fails = (bDepth ? 1 : 0) + (bFall ? 1 : 0) + (bTr ? 1 : 0); + if (fails == 1) + { + if (bDepth) d.SoleDepth++; else if (bFall) d.SoleFalloff++; else d.SoleTrench++; + } + + float z = IslandFalloff.OffshoreZoneWeight(depthM, preTrench[x, y], + MathF.Abs(x - centerX) / halfSpan, MathF.Abs(y - centerY) / halfSpan, + s.MinDepthM, s.DepthFeatherM, s.MinFalloff, s.FalloffFeather, s.TrenchInner, s.TrenchOuter); + zone[x, y] = z; + if (z > 0f) { d.Zone++; zoneRows[hi]++; colZone[hi]++; } + if (z >= 1f) d.ZoneFull++; + } + for (int hi = 0; hi < 2; hi++) if (colZone[hi] == 0) colsNoZone[hi]++; + } + rep.North.OceanRowsPerColumn = oceanRows[0] / (double)mapSize; + rep.South.OceanRowsPerColumn = oceanRows[1] / (double)mapSize; + rep.North.ZoneRowsPerColumn = zoneRows[0] / (double)mapSize; + rep.South.ZoneRowsPerColumn = zoneRows[1] / (double)mapSize; + rep.North.ColumnsWithNoZone = colsNoZone[0]; + rep.South.ColumnsWithNoZone = colsNoZone[1]; + + // Peaks: strict local maxima of v over the 8-neighbourhood, on sea cells. + for (int x = 0; x < mapSize; x++) + { + for (int y = 0; y < mapSize; y++) + { + if (zone[x, y] < 0f) continue; // land + float c = v[x, y]; + bool isMax = true; + for (int dx = -1; dx <= 1 && isMax; dx++) + { + int nx = x + dx; if (nx < 0 || nx >= mapSize) continue; + for (int dy = -1; dy <= 1; dy++) + { + if (dx == 0 && dy == 0) continue; + int ny = y + dy; if (ny < 0 || ny >= mapSize) continue; + if (v[nx, ny] >= c) { isMax = false; break; } + } + } + if (!isMax) continue; + + var d = y < half ? rep.North : rep.South; + d.PeaksSea++; + float thr = s.Mode == OffshoreMode.Faithful ? thrN : OffshorePass.BlendedThreshold(y, mid, band, thrN, thrS); + bool over = c > thr; + if (zone[x, y] > 0f) + { + d.PeaksInZone++; + if (over) d.PeaksInZoneOverThr++; + } + else if (over) + { + d.PeaksOverThrLost++; + float h = height[x, y]; + float depthM = WorldScale.MetresFromRaw(sea - h); + float dist = MathF.Max(MathF.Abs(x - centerX) / halfSpan, MathF.Abs(y - centerY) / halfSpan); + if (depthM < s.MinDepthM) d.LostDepth++; + if (preTrench[x, y] < s.MinFalloff) d.LostFalloff++; + if (dist >= s.TrenchOuter) d.LostTrench++; + } + } + } + return rep; + } + + /// One markdown row per hemisphere for a report table (see ). + public static string TableHeader() => + "| seed | hemi | sea cells | zone cells | zone / sea | zone == 1 | ocean rows/col | zone rows/col | cols w/o zone | " + + "blocked: depth / falloff / trench | sole blocker: depth / falloff / trench | peaks: sea / in zone / in zone > thr | lost > thr (depth / falloff / trench) | candidates per Mcell |\n" + + "|---|---|---|---|---|---|---|---|---|---|---|---|---|---|"; + + public static string TableRow(Report r, HemisphereDiagnosis d) => + $"| `{r.Seed}` | **{d.Name}** | {d.Sea:N0} | {d.Zone:N0} | {d.ZoneShareOfSea:P1} | {d.ZoneFull:N0} | {d.OceanRowsPerColumn:F0} | {d.ZoneRowsPerColumn:F0} | {d.ColumnsWithNoZone} | " + + $"{d.BlockDepth:N0} / {d.BlockFalloff:N0} / {d.BlockTrench:N0} | {d.SoleDepth:N0} / {d.SoleFalloff:N0} / {d.SoleTrench:N0} | " + + $"{d.PeaksSea} / {d.PeaksInZone} / **{d.PeaksInZoneOverThr}** | {d.PeaksOverThrLost} ({d.LostDepth} / {d.LostFalloff} / {d.LostTrench}) | {d.CandidatesPerMegacell:F1} |"; + + /// Sum a pool of reports per hemisphere (means for the per-column numbers). + public static (HemisphereDiagnosis north, HemisphereDiagnosis south) Pool(IReadOnlyList reports) + { + var n = new HemisphereDiagnosis { Name = "north (pool)" }; + var s = new HemisphereDiagnosis { Name = "south (pool)" }; + foreach (var r in reports) { Add(n, r.North); Add(s, r.South); } + int k = Math.Max(1, reports.Count); + n.OceanRowsPerColumn /= k; s.OceanRowsPerColumn /= k; + n.ZoneRowsPerColumn /= k; s.ZoneRowsPerColumn /= k; + n.ColumnsWithNoZone /= k; s.ColumnsWithNoZone /= k; + return (n, s); + } + + private static void Add(HemisphereDiagnosis a, HemisphereDiagnosis b) + { + a.Sea += b.Sea; a.Zone += b.Zone; a.ZoneFull += b.ZoneFull; + a.BlockDepth += b.BlockDepth; a.BlockFalloff += b.BlockFalloff; a.BlockTrench += b.BlockTrench; + a.SoleDepth += b.SoleDepth; a.SoleFalloff += b.SoleFalloff; a.SoleTrench += b.SoleTrench; + a.OceanRowsPerColumn += b.OceanRowsPerColumn; a.ZoneRowsPerColumn += b.ZoneRowsPerColumn; a.ColumnsWithNoZone += b.ColumnsWithNoZone; + a.PeaksSea += b.PeaksSea; a.PeaksInZone += b.PeaksInZone; a.PeaksInZoneOverThr += b.PeaksInZoneOverThr; + a.PeaksOverThrLost += b.PeaksOverThrLost; a.LostDepth += b.LostDepth; a.LostFalloff += b.LostFalloff; a.LostTrench += b.LostTrench; + } + + /// A one-paragraph reading of the pooled numbers: which hemisphere has less eligible ocean, and which gate binds it. + public static string Interpret(HemisphereDiagnosis n, HemisphereDiagnosis s) + { + var sb = new StringBuilder(); + string smaller = n.Zone < s.Zone ? "NORTH" : "SOUTH"; + double ratio = n.Zone == 0 || s.Zone == 0 ? 0 : (double)Math.Max(n.Zone, s.Zone) / Math.Min(n.Zone, s.Zone); + sb.Append($"Valid offshore zone: north {n.Zone:N0} cells ({n.ZoneShareOfSea:P1} of its ocean, {n.ZoneRowsPerColumn:F0} rows/col), " + + $"south {s.Zone:N0} cells ({s.ZoneShareOfSea:P1} of its ocean, {s.ZoneRowsPerColumn:F0} rows/col) — the {smaller} has " + + $"{ratio:F2}× less island-eligible ocean. "); + sb.Append($"Island candidates (peaks in zone clearing the threshold): north {n.PeaksInZoneOverThr}, south {s.PeaksInZoneOverThr}; " + + $"capacity (all peaks in zone): north {n.PeaksInZone}, south {s.PeaksInZone}. "); + string Bind(HemisphereDiagnosis d) + { + long max = Math.Max(d.SoleDepth, Math.Max(d.SoleFalloff, d.SoleTrench)); + string g = max == d.SoleFalloff ? "the falloff test" : max == d.SoleDepth ? "the moat (ambient depth)" : "the outer/trench bound"; + long lostMax = Math.Max(d.LostDepth, Math.Max(d.LostFalloff, d.LostTrench)); + string lg = d.PeaksOverThrLost == 0 ? "none" : lostMax == d.LostFalloff ? "falloff" : lostMax == d.LostDepth ? "moat" : "outer bound"; + return $"{d.Name}: binding gate by sole-blocked cells = {g} (depth {d.SoleDepth:N0} / falloff {d.SoleFalloff:N0} / trench {d.SoleTrench:N0}); " + + $"over-threshold peaks lost to gates = {d.PeaksOverThrLost} (mostly {lg})"; + } + sb.Append(Bind(n)).Append(". ").Append(Bind(s)).Append('.'); + return sb.ToString(); + } + } +} diff --git a/Tools/Scripts/OffshoreDiagnosis.cs.uid b/Tools/Scripts/OffshoreDiagnosis.cs.uid new file mode 100644 index 0000000..40731a6 --- /dev/null +++ b/Tools/Scripts/OffshoreDiagnosis.cs.uid @@ -0,0 +1 @@ +uid://mriontjn1ihq diff --git a/Tools/Scripts/OffshoreIslandsTool.cs b/Tools/Scripts/OffshoreIslandsTool.cs index dac74fb..f84c755 100644 --- a/Tools/Scripts/OffshoreIslandsTool.cs +++ b/Tools/Scripts/OffshoreIslandsTool.cs @@ -8,53 +8,75 @@ using IslaApocalypse.Core; namespace IslaApocalypse.Tools { /// - /// ⭐ THE OFFSHORE-ISLANDS BATCH (chat2/05) — the faithful rejoin as the control, then the - /// reshape: small / low / flat / rigid, loose-guaranteed (≥2 N, ≥4 S), organic extras weighted - /// south, corners on. Judged on 2D maps, with the tag made visible. + /// ⭐ THE OFFSHORE-ISLANDS BATCH — chat2/06: ORGANIC-ONLY, TUNED FOR COVERAGE, SOUTH-WEIGHTED, + /// NO FORCED COUNT. (chat2/05's version of this tool — faithful / floor_only / hybrid / dense — + /// is in git history at 3b96e06; the forced floor it batched was reverted out on look.) /// - /// ═══ THE VARIANTS ═══ + /// ═══ WHAT IT PRODUCES — a fixed budget: 4 plates + a count table + a diagnosis ═══ /// - /// faithful stage 1 — the reference's probabilistic islets, verbatim. Sparse, no corners, - /// no floor. THE CONTROL. - /// floor_only the reshape's seeded ≥2N/4S floor with the organic layer OFF — shows the - /// guaranteed minimum and that positions vary per seed. - /// hybrid ⭐ floor + organic extras, reshaped, corners on. THE DELIVERABLE. Six seeds. - /// dense hybrid with ×3 organic density — a bookend for "how many is too many". + /// PLATES (4, at ISLA_MAPSIZE, grayscale + .f32 + relief + tags overlay): + /// {plate}_density_low / _mid / _high three densities on ONE seed — the developer picks the + /// look by eye (more islands vs slop), apples to apples. + /// {bulge}_density_mid the preset on the southern-bulge seed — the table seed + /// whose south was SPARSEST at density_mid (auto-picked, + /// or ISLA_BULGE_SEED) — the south fix is not seed-specific. /// - /// The shelf is ON for every variant (it is stage 1's other half and is invisible on these - /// plates anyway); an offshore-OFF / shelf-OFF field is generated per seed for the oracle only. + /// THE COUNT TABLE (data, not plates): per seed, north / south island counts across ~12 seeds for + /// each of the 3 density levels, with min / mean / max per hemisphere per level, the guard + /// ledger and the island sizes. This is how "a few south / a couple north, consistently" is + /// READ — as statistics of the tuning, never as a floor. + /// + /// THE DIAGNOSIS (data): per hemisphere over the calibration seed pool — valid-zone area, which + /// gate binds, noise peaks clearing the threshold — measured BEFORE any knob moved (§2 of the + /// task). → . /// /// ═══ THE CURVE IS THE TAGGED CURVE, UNCHANGED ═══ /// /// `continuous_restored` (tag terrain-curve-v1), calibrated on task 01's pool at the iteration /// size WITH OFFSHORE OFF — islands are additive land on top of a curve that does not know they - /// exist. Their crest (24–34 m pre-curve) lands inside the curve's preserved toe, which squashes - /// it to a few metres; because that toe is bit-preserved across curve tweaks, the islands' height - /// is stable however the upper climb is tuned later. + /// exist. Oracle a1 / a3 / a4 prove offshore-off is bit-identical to Phase 1, task 03 and the + /// tag's own 04 gallery dump. /// /// ═══ RUNNING IT ═══ /// /// xvfb-run -a Godot_v4.7.2-stable_mono_linux.x86_64 \ /// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/OffshoreIslandsTool.tscn /// - /// ISLA_TASK / ISLA_BATCH / ISLA_SKIP_RAW - /// ISLA_MAPSIZE render size (default 4096 — islands need pixels to read) - /// ISLA_CALIB_SIZE curve calibration size (default 2048, task 01's) - /// ISLA_SEEDS_HYBRID the six hybrid seeds - /// ISLA_SEEDS_SMALL the three seeds for faithful / floor_only / dense - /// ISLA_PHASE1_SOURCE Phase-1 .f32 batch (default "02_pass1_port") - /// ISLA_T03_SOURCE task-03 .f32 batch (default "03_mountain_restore") + /// ISLA_TASK / ISLA_BATCH / ISLA_SKIP_RAW / ISLA_OUTPUT_DIR + /// ISLA_MAPSIZE plate + table size (default 4096 — islands need pixels to read) + /// ISLA_TABLE_SIZE count-table size (default = ISLA_MAPSIZE; a probe may drop it) + /// ISLA_CALIB_SIZE curve calibration size (default 2048, task 01's) — also the diagnosis size + /// ISLA_TABLE_SEEDS the count-table seeds (default 12 below) + /// ISLA_PLATE_SEED the three-density seed (default 1063685222) + /// ISLA_BULGE_SEED the south-bulge seed (default 0 = auto: sparsest south at density_mid) + /// ISLA_DENS_LOW / ISLA_DENS_MID / ISLA_DENS_HIGH / ISLA_SOUTH_WEIGHT the tuning (probe overrides) + /// ISLA_OFF_MINAREA / ISLA_OFF_MINSEP / ISLA_OFF_MAXAREA the guards (probe overrides) + /// ISLA_OFF_FREQ / ISLA_OFF_CORE / ISLA_OFF_SHARP / ISLA_OFF_CREST the shape (probe overrides) + /// ISLA_TABLE_ONLY=1 probe: diagnosis + count table only (no regressions, no plates) + /// ISLA_SKIP_8K=1 skip the 8192 regression against the 04 gallery dump (a4) + /// ISLA_PHASE1_SOURCE / ISLA_T03_SOURCE / ISLA_T04_SOURCE the regression dumps' batches /// public partial class OffshoreIslandsTool : Node { - private static readonly int[] DefaultHybridSeeds = { 1063685222, 20260821, 8675309, 123456789, 271828182, 999999937 }; - private static readonly int[] DefaultSmallSeeds = { 1063685222, 8675309, 999999937 }; + /// + /// The count-table pool: chat2/05's six hybrid seeds + task 01's calibration pool (minus the + /// shared anchor) + one more. Twelve draws; the anchor first. + /// + private static readonly int[] DefaultTableSeeds = + { + 1063685222, 20260821, 8675309, 123456789, 271828182, 999999937, + 20260819, 777001, 424242, 90210, 31337, 55555, + }; - /// ⚠ Task 01's pool, verbatim — the curve's identity. + /// ⚠ Task 01's pool, verbatim — the curve's identity. Also the diagnosis pool. private static readonly int[] CalibrationSeeds = { 1063685222, 20260819, 777001, 424242, 90210, 31337 }; private const int DefaultMapSize = 4096; private const int DefaultCalibSize = 2048; + private const int GallerySize = 8192; // the 04 gallery's render size + + /// The consistency targets the table is read against: "a couple north, a few south". + private const int TargetNorth = 2, TargetSouth = 3; public override void _Ready() { @@ -63,6 +85,7 @@ namespace IslaApocalypse.Tools { GD.PrintErr("=================================================================="); GD.PrintErr($" REFUSED: {e.Message}"); + GD.PrintErr(e.StackTrace); GD.PrintErr("=================================================================="); GetTree().Quit(2); } @@ -70,26 +93,36 @@ namespace IslaApocalypse.Tools private sealed class Row { - public string Variant; public int Seed; - public int CountN, CountS; public long Lifted; public float HMaxBefore, HMaxAfter; - public bool Ok; - public string Centres; // the seeded floor's (x,y) list — the "positions vary per seed" evidence + public string Level; public int Seed; + public int CountN, CountS, PreN, PreS, SpecksN, SpecksS, ClustersN, ClustersS, BlobsN, BlobsS; + public long Lifted, SizeMin, SizeMed, SizeMax; public double SizeMean; + public float HMaxBefore, HMaxAfter; + public bool Ok; public ulong Ms; + } + + private sealed class Level + { + public string Label; public OffshoreSettings Settings; } private void Run() { ToolingPaths.Configure(OS.GetUserDataDir()); - int task = EnvInt("ISLA_TASK", 5); - string descr = EnvStr("ISLA_BATCH", "offshore_islands"); + int task = EnvInt("ISLA_TASK", 6); + string descr = EnvStr("ISLA_BATCH", "offshore_organic_tune"); int mapSize = EnvInt("ISLA_MAPSIZE", DefaultMapSize); + int tableSize = EnvInt("ISLA_TABLE_SIZE", mapSize); int calibSize = EnvInt("ISLA_CALIB_SIZE", DefaultCalibSize); - int[] hybridSeeds = EnvSeeds("ISLA_SEEDS_HYBRID", DefaultHybridSeeds); - int[] smallSeeds = EnvSeeds("ISLA_SEEDS_SMALL", DefaultSmallSeeds); + int[] tableSeeds = EnvSeeds("ISLA_TABLE_SEEDS", DefaultTableSeeds); + int plateSeed = EnvInt("ISLA_PLATE_SEED", 1063685222); + int bulgeSeedEnv = EnvInt("ISLA_BULGE_SEED", 0); string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port"); string t03Source = EnvStr("ISLA_T03_SOURCE", "03_mountain_restore"); + string t04Source = EnvStr("ISLA_T04_SOURCE", "04_seed_gallery"); bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1"; - string only = EnvStr("ISLA_ONLY", null); // probe: comma list of variant labels to run + bool tableOnly = EnvStr("ISLA_TABLE_ONLY", "0") == "1"; + bool skip8k = EnvStr("ISLA_SKIP_8K", "0") == "1"; string batchRoot = ToolingPaths.BatchRoot(task, descr); DirAccess.MakeDirRecursiveAbsolute(batchRoot); @@ -97,39 +130,63 @@ namespace IslaApocalypse.Tools var anchors = CurveAnchors.Default; float sea = 0.15f; - int primary = hybridSeeds[0]; + + // ═══ THE THREE DENSITY LEVELS — the preset of record is `mid` ═══ + OffshoreSettings LevelSettings(float density) + { + var o = OffshoreSettings.Organic(); + o.Density = density; + o.SouthWeight = EnvFloat("ISLA_SOUTH_WEIGHT", o.SouthWeight); + o.MinIslandAreaFrac = EnvFloat("ISLA_OFF_MINAREA", o.MinIslandAreaFrac); + o.MinSeparationFrac = EnvFloat("ISLA_OFF_MINSEP", o.MinSeparationFrac); + o.MaxIslandAreaFrac = EnvFloat("ISLA_OFF_MAXAREA", o.MaxIslandAreaFrac); + o.FreqPerMapWidth = EnvFloat("ISLA_OFF_FREQ", o.FreqPerMapWidth); + o.CoreFraction = EnvFloat("ISLA_OFF_CORE", o.CoreFraction); + o.EdgeSharpness = EnvFloat("ISLA_OFF_SHARP", o.EdgeSharpness); + o.CrestM = EnvFloat("ISLA_OFF_CREST", o.CrestM); + return o; + } + var levels = new List + { + new() { Label = "density_low", Settings = LevelSettings(EnvFloat("ISLA_DENS_LOW", OffshoreSettings.OrganicDensityLow)) }, + new() { Label = "density_mid", Settings = LevelSettings(EnvFloat("ISLA_DENS_MID", OffshoreSettings.OrganicDensityMid)) }, + new() { Label = "density_high", Settings = LevelSettings(EnvFloat("ISLA_DENS_HIGH", OffshoreSettings.OrganicDensityHigh)) }, + }; + Level mid = levels[1]; GD.Print("=================================================================="); - GD.Print(" OFFSHORE ISLANDS (chat2/05) — faithful rejoin, then the reshape"); + GD.Print(" OFFSHORE ISLANDS (chat2/06) — organic-only, tuned for coverage, south-weighted, no forced count"); GD.Print("=================================================================="); - GD.Print($"MapSize : {mapSize} curve calibrated at {calibSize} (offshore OFF)"); - GD.Print($"hybrid : {string.Join(", ", hybridSeeds)}"); - GD.Print($"others : {string.Join(", ", smallSeeds)}"); + GD.Print($"MapSize : {mapSize} (plates) table at {tableSize} curve calibrated at {calibSize} (offshore OFF) diagnosis at {calibSize}"); + GD.Print($"table : {string.Join(", ", tableSeeds)}"); + GD.Print($"plate seed: {plateSeed} bulge seed: {(bulgeSeedEnv > 0 ? bulgeSeedEnv.ToString() : "auto (sparsest south at density_mid)")}"); + foreach (var l in levels) GD.Print($" {l.Label,-13} {l.Settings.Describe()}"); GD.Print($"hemisphere: NORTH = rows [0, {mapSize / 2}) SOUTH = rows [{mapSize / 2}, {mapSize}) (y runs south)"); - GD.Print($"batch : {batchRoot}"); + GD.Print($"batch : {batchRoot}{(tableOnly ? " ⚠ ISLA_TABLE_ONLY — a probe, not the batch of record" : "")}"); GD.Print("=================================================================="); // ═══ 0. THE CURVE — continuous_restored, calibrated with offshore off ═══ GD.Print($"\n--- 0. CURVE (task-01 pool at {calibSize}, offshore off) ---"); - var (knots, calibration) = CalibrateCurve(calibSize, sea, anchors); + var (knots, calibration, _) = CalibrateCurve(calibSize, sea, anchors); GD.Print($" {knots}"); GD.Print($" {calibration.Describe()}"); - // ═══ 1. REGRESSIONS at the calibration size — the things that must not have moved ═══ - GD.Print($"\n--- 1. REGRESSIONS at {calibSize}, seed {primary} ---"); + // ═══ 1. REGRESSIONS — the things that must not have moved ═══ var hard = new List(); + if (!tableOnly) { - var offCfg = BaseConfig(calibSize, primary, knots, anchors, calibration, "off"); + GD.Print($"\n--- 1. REGRESSIONS at {calibSize}, seed {plateSeed} ---"); + var offCfg = BaseConfig(calibSize, plateSeed, knots, anchors, calibration, "off"); Pass1Result p1 = Topography.Generate(offCfg); var curveOff = offCfg.Clone(); curveOff.Curve = false; Pass2Result pOff = Shaping.Shape(p1, curveOff); - string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{primary}_full", "height.f32"); + string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{plateSeed}_full", "height.f32"); hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF, offshore OFF == Phase-1 .f32 dump", pOff.Height, HeightField.Load(p1Dump, calibSize), calibSize, p1Dump)); Pass2Result pRest = Shaping.Shape(p1, offCfg); - string t03Dump = Path.Combine(ToolingPaths.BatchesRoot, t03Source, $"{primary}_continuous_restored", "height.f32"); + string t03Dump = Path.Combine(ToolingPaths.BatchesRoot, t03Source, $"{plateSeed}_continuous_restored", "height.f32"); hard.Add(ShapingOracle.DumpRegression("a3", "continuous_restored, offshore OFF == task-03 .f32 dump (lowlands + curve untouched)", pRest.Height, HeightField.Load(t03Dump, calibSize), calibSize, t03Dump)); @@ -140,103 +197,132 @@ namespace IslaApocalypse.Tools j0.Name = "shelf alone: every land cell bit-identical (shelf is below-sea only)"; hard.Add(j0); hard.Add(ShapingOracle.HMaxAfterOffshore(p1Shelf)); - } - foreach (var c in hard) GD.Print(" " + c); + foreach (var c in hard) GD.Print(" " + c); - // ═══ 2. VARIANTS ═══ - // ⚠ PROBE OVERRIDES on the reshape's organic knobs, so the hybrid can be swept in scratch - // without a rebuild. The defaults live in OffshoreSettings.Hybrid() — that is the - // preset of record; these move it only when set. Printed into the INDEX either way. - OffshoreSettings HybridTuned() - { - var h = OffshoreSettings.Hybrid(); - h.FreqPerMapWidth = EnvFloat("ISLA_OFF_FREQ", h.FreqPerMapWidth); - h.DensityNorth = EnvFloat("ISLA_OFF_DENS_N", h.DensityNorth); - h.DensitySouth = EnvFloat("ISLA_OFF_DENS_S", h.DensitySouth); - h.CoreFraction = EnvFloat("ISLA_OFF_CORE", h.CoreFraction); - h.EdgeSharpness = EnvFloat("ISLA_OFF_SHARP", h.EdgeSharpness); - h.MinIslandAreaFrac = EnvFloat("ISLA_OFF_MINAREA", h.MinIslandAreaFrac); - h.CrestM = EnvFloat("ISLA_OFF_CREST", h.CrestM); - return h; - } - - var variants = new List<(string label, int[] seeds, Action mutate)> - { - ("faithful", smallSeeds, c => { c.CoastShelf = true; c.Offshore = OffshoreSettings.Faithful(); }), - ("floor_only", smallSeeds, c => { c.CoastShelf = true; c.Offshore = HybridTuned(); c.Offshore.Organic = false; }), - ("hybrid", hybridSeeds, c => { c.CoastShelf = true; c.Offshore = HybridTuned(); }), - ("dense", smallSeeds, c => { c.CoastShelf = true; c.Offshore = HybridTuned(); - c.Offshore.DensityNorth *= 3f; c.Offshore.DensitySouth *= 3f; }), - }; - if (!string.IsNullOrWhiteSpace(only)) - { - var keep = new HashSet(only.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)); - variants.RemoveAll(v => !keep.Contains(v.label)); - GD.Print($" ⚠ ISLA_ONLY: running {string.Join(", ", keep)} only (a probe, not the batch of record)"); - } - - GD.Print($"\n--- 2. VARIANTS at {mapSize} ---"); - var offFields = new Dictionary(); // per seed, shelf off + offshore off - var rows = new List(); - var perVariantChecks = new List(); - var knobs = new Dictionary(); - bool notesShown = false; - - foreach (var (label, seeds, mutate) in variants) - { - foreach (int seed in seeds) + // ⭐ a4 — offshore OFF at the 04 gallery's size == the terrain-curve-v1 tag's OWN output. + // The literal "offshore-off is bit-identical to terrain-curve-v1", at full size. + if (!skip8k) { - if (!offFields.TryGetValue(seed, out Pass1Result p1Off)) + string t04Dump = Path.Combine(ToolingPaths.BatchesRoot, t04Source, $"{plateSeed}", "height.f32"); + if (File.Exists(t04Dump)) { - p1Off = Topography.Generate(BaseConfig(mapSize, seed, knots, anchors, calibration, "off")); - offFields[seed] = p1Off; + GD.Print($" a4: generating {plateSeed} at {GallerySize}, offshore OFF, against {t04Dump} …"); + var gCfg = BaseConfig(GallerySize, plateSeed, knots, anchors, calibration, "off"); + Pass2Result pG = Shaping.Shape(Topography.Generate(gCfg), gCfg); + var a4 = ShapingOracle.DumpRegression("a4", $"offshore OFF at {GallerySize} == terrain-curve-v1's 04 gallery .f32 dump", + pG.Height, HeightField.Load(t04Dump, GallerySize), GallerySize, t04Dump); + hard.Add(a4); + GD.Print(" " + a4); } + else GD.Print($" a4: ⚠ skipped — no 04 gallery dump at {t04Dump}"); + } + else GD.Print(" a4: skipped (ISLA_SKIP_8K)"); + } - var cfg = BaseConfig(mapSize, seed, knots, anchors, calibration, label); - mutate(cfg); - knobs[label] = cfg.Offshore.Describe(); + // ═══ 2. THE DIAGNOSIS — measure the south before touching a knob ═══ + GD.Print($"\n--- 2. DIAGNOSIS (calibration pool at {calibSize}, shelf on, {mid.Label} thresholds) ---"); + var diag = new List(); + foreach (int s in CalibrationSeeds) + { + var dCfg = BaseConfig(calibSize, s, knots, anchors, calibration, "shelf_only"); + dCfg.CoastShelf = true; + Pass1Result pShelf = Topography.Generate(dCfg); + var rep = OffshoreDiagnosis.Run(pShelf.Height, pShelf.PreTrenchFalloff, calibSize, s, sea, mid.Settings, dCfg.Scale); + diag.Add(rep); + GD.Print($" seed {s,-11} N: zone {rep.North.Zone,9:N0} ({rep.North.ZoneShareOfSea,6:P1}) peaks in zone {rep.North.PeaksInZone,3} > thr {rep.North.PeaksInZoneOverThr,3} " + + $"S: zone {rep.South.Zone,9:N0} ({rep.South.ZoneShareOfSea,6:P1}) peaks in zone {rep.South.PeaksInZone,3} > thr {rep.South.PeaksInZoneOverThr,3} " + + $"sole-blocked N d/f/t {rep.North.SoleDepth:N0}/{rep.North.SoleFalloff:N0}/{rep.North.SoleTrench:N0} S {rep.South.SoleDepth:N0}/{rep.South.SoleFalloff:N0}/{rep.South.SoleTrench:N0}"); + } + var (poolN, poolS) = OffshoreDiagnosis.Pool(diag); + string interpretation = OffshoreDiagnosis.Interpret(poolN, poolS); + GD.Print(" " + interpretation); + // ═══ 3. THE COUNT TABLE — ~12 seeds × 3 levels ═══ + GD.Print($"\n--- 3. COUNT TABLE at {tableSize} ---"); + var rows = new List(); + var perFieldChecks = new List(); + bool notesShown = false; + foreach (int seed in tableSeeds) + { + Pass1Result p1Off = Topography.Generate(BaseConfig(tableSize, seed, knots, anchors, calibration, "off")); + foreach (var lv in levels) + { + var cfg = BaseConfig(tableSize, seed, knots, anchors, calibration, lv.Label); + cfg.CoastShelf = true; cfg.Offshore = lv.Settings.Clone(); Pass1Result p1 = Topography.Generate(cfg); Pass2Result p2 = Shaping.Shape(p1, cfg); + if (!notesShown) { foreach (string n in p1.Notes) GD.Print(" " + n); notesShown = true; } - if (!notesShown || label == "hybrid" && seed == primary) - foreach (string n in p1.Notes) GD.Print(" " + n); - notesShown = true; - - // ---- oracle, per field ---- - var comps = OffshoreAnalysis.Components(p1.IsOffshoreIsland, p1.Height, sea, mapSize); + var comps = OffshoreAnalysis.Components(p1.IsOffshoreIsland, p1.Height, sea, tableSize); var (cn, cs) = OffshoreAnalysis.CountByHemisphere(comps); - var checks = new List(); - if (cfg.Offshore.FloorNorth > 0 || cfg.Offshore.FloorSouth > 0) - checks.Add(ShapingOracle.OffshoreFloor(p1, cfg.Offshore.FloorNorth, cfg.Offshore.FloorSouth, sea, comps)); - checks.Add(ShapingOracle.MoatIntact(p1, comps)); - checks.Add(ShapingOracle.MainlandUnmoved(p1Off, p1, sea)); - checks.Add(ShapingOracle.TagCoastlineConsistent(p2, sea)); - checks.Add(ShapingOracle.HMaxAfterOffshore(p1)); - checks.Add(ShapingOracle.ClassifyFidelity(p1, p2)); - foreach (var c in checks) { c.Name += $" [{label} {seed}]"; perVariantChecks.Add(c); } + var (szMin, szMed, szMean, szMax, _) = OffshoreAnalysis.SizeSummary(comps, 0); + var checks = new List + { + ShapingOracle.MoatIntact(p1, comps), + ShapingOracle.MainlandUnmoved(p1Off, p1, sea), + ShapingOracle.TagCoastlineConsistent(p2, sea), + ShapingOracle.HMaxAfterOffshore(p1), + ShapingOracle.ClassifyFidelity(p1, p2), + }; + foreach (var c in checks) { c.Name += $" [{lv.Label} {seed}]"; perFieldChecks.Add(c); } bool ok = checks.TrueForAll(c => c.Passed); - WriteVariant(batchRoot, p1, p2, sea, anchors, skipRaw, cn, cs); - - var cl = new List(); - foreach (var (cx, cy, _) in p1.OffshoreCentres) cl.Add($"({cx},{cy})"); - rows.Add(new Row + var row = new Row { - Variant = label, Seed = seed, CountN = cn, CountS = cs, - Lifted = p1.OffshoreLiftedCells, HMaxBefore = p1.HMaxSeedBeforeOffshore, HMaxAfter = p1.HMaxSeed, Ok = ok, - Centres = cl.Count == 0 ? "—" : string.Join(" ", cl), - }); - GD.Print($" {label,-11} seed {seed,-11} islands N {cn,2} S {cs,2} lifted {p1.OffshoreLiftedCells,8:N0} " + - $"hMax {p1.HMaxSeedBeforeOffshore:F4}→{p1.HMaxSeed:F4} {(ok ? "ok" : "⚠ CHECK FAILED")} {p1.ElapsedMs} ms"); + Level = lv.Label, Seed = seed, CountN = cn, CountS = cs, + Lifted = p1.OffshoreLiftedCells, HMaxBefore = p1.HMaxSeedBeforeOffshore, HMaxAfter = p1.HMaxSeed, + SizeMin = szMin, SizeMed = szMed, SizeMean = szMean, SizeMax = szMax, Ok = ok, Ms = p1.ElapsedMs, + }; + ReadGuardLedger(p1, row); + rows.Add(row); + GD.Print($" {lv.Label,-13} seed {seed,-11} N {cn,2} S {cs,2} (pre-guard N {row.PreN,2} S {row.PreS,2}; specks {row.SpecksN + row.SpecksS,2} clusters {row.ClustersN + row.ClustersS,2} blobs {row.BlobsN + row.BlobsS,2}) " + + $"size med {szMed,5} max {szMax,6} {(ok ? "ok" : "⚠ CHECK FAILED")} {p1.ElapsedMs} ms"); + } + } + var stats = LevelStats(levels, rows); + GD.Print("\n per level (min / mean / max):"); + foreach (var st in stats) + GD.Print($" {st.Label,-13} N {st.MinN} / {st.MeanN:F1} / {st.MaxN} S {st.MinS} / {st.MeanS:F1} / {st.MaxS} " + + $"seeds with N≥{TargetNorth} & S≥{TargetSouth}: {st.MeetBoth}/{st.Seeds} S≥N: {st.SouthAtLeastNorth}/{st.Seeds} " + + $"guards: specks {st.Specks} clusters {st.Clusters} blobs {st.Blobs}"); + + // ═══ 4. THE PLATES — exactly four ═══ + int bulgeSeed = bulgeSeedEnv > 0 ? bulgeSeedEnv : PickBulgeSeed(rows, mid.Label, plateSeed); + GD.Print($"\n southern-bulge seed: {bulgeSeed}{(bulgeSeedEnv > 0 ? " (ISLA_BULGE_SEED)" : " (auto: sparsest south at density_mid among the table seeds)")}"); + var plates = new List<(int seed, Level level)> { (plateSeed, levels[0]), (plateSeed, levels[1]), (plateSeed, levels[2]), (bulgeSeed, mid) }; + var plateRows = new List(); + if (!tableOnly) + { + GD.Print($"\n--- 4. PLATES at {mapSize} ---"); + foreach (var (seed, lv) in plates) + { + var cfg = BaseConfig(mapSize, seed, knots, anchors, calibration, lv.Label); + cfg.CoastShelf = true; cfg.Offshore = lv.Settings.Clone(); + Pass1Result p1 = Topography.Generate(cfg); + Pass2Result p2 = Shaping.Shape(p1, cfg); + var comps = OffshoreAnalysis.Components(p1.IsOffshoreIsland, p1.Height, sea, mapSize); + var (cn, cs) = OffshoreAnalysis.CountByHemisphere(comps); + var moat = ShapingOracle.MoatIntact(p1, comps); moat.Name += $" [plate {lv.Label} {seed}]"; + var tag = ShapingOracle.TagCoastlineConsistent(p2, sea); tag.Name += $" [plate {lv.Label} {seed}]"; + perFieldChecks.Add(moat); perFieldChecks.Add(tag); + WritePlate(batchRoot, p1, p2, sea, anchors, skipRaw, cn, cs); + var row = new Row { Level = lv.Label, Seed = seed, CountN = cn, CountS = cs, Lifted = p1.OffshoreLiftedCells, Ok = moat.Passed && tag.Passed }; + ReadGuardLedger(p1, row); + plateRows.Add(row); + GD.Print($" plate {seed}_{lv.Label}: N {cn} S {cs} lifted {p1.OffshoreLiftedCells:N0} {(row.Ok ? "ok" : "⚠ CHECK FAILED")} {p1.ElapsedMs} ms"); + foreach (string n in p1.Notes) if (n.Contains("guards") || n.Contains("islands:")) GD.Print(" " + n); } } - bool allOk = hard.TrueForAll(c => c.Passed) && perVariantChecks.TrueForAll(c => c.Passed); + bool allOk = hard.TrueForAll(c => c.Passed) && perFieldChecks.TrueForAll(c => c.Passed); GD.Print($"\n ORACLE: {(allOk ? "ALL HARD CHECKS PASS" : "*** FAILURES ***")}"); - foreach (var c in perVariantChecks) if (!c.Passed) GD.PrintErr(" " + c); + foreach (var c in perFieldChecks) if (!c.Passed) GD.PrintErr(" " + c); - WriteIndex(batchRoot, mapSize, calibSize, primary, hybridSeeds, smallSeeds, rows, knobs, hard, perVariantChecks, allOk); + // ═══ 5. THE DATA FILES + INDEX ═══ + WriteCountTable(batchRoot, tableSize, levels, rows, stats); + WriteDiagnosis(batchRoot, calibSize, mid, diag, poolN, poolS, interpretation); + WriteIndex(batchRoot, mapSize, tableSize, calibSize, plateSeed, bulgeSeed, tableSeeds, levels, rows, stats, plateRows, + diag, poolN, poolS, interpretation, hard, perFieldChecks, allOk, tableOnly); GD.Print("\n=================================================================="); GD.Print($" DONE — {batchRoot}"); @@ -245,9 +331,68 @@ namespace IslaApocalypse.Tools GetTree().Quit(allOk ? 0 : 3); } - // ---- the curve, measured exactly as tasks 03/04 did -------------------- + // ---- the table's statistics ------------------------------------------- - private static (CurveKnots, ClimbCalibration) CalibrateCurve(int calibSize, float sea, CurveAnchors anchors) + private sealed class LevelStat + { + public string Label; public int Seeds; + public int MinN, MaxN, MinS, MaxS; public double MeanN, MeanS; + public int MeetBoth, SouthAtLeastNorth, Specks, Clusters, Blobs; + public long SizeMed, SizeMax; + } + + private static List LevelStats(List levels, List rows) + { + var outp = new List(); + foreach (var lv in levels) + { + var st = new LevelStat { Label = lv.Label, MinN = int.MaxValue, MinS = int.MaxValue }; + double sumN = 0, sumS = 0; var meds = new List(); + foreach (var r in rows) + { + if (r.Level != lv.Label) continue; + st.Seeds++; + st.MinN = Math.Min(st.MinN, r.CountN); st.MaxN = Math.Max(st.MaxN, r.CountN); sumN += r.CountN; + st.MinS = Math.Min(st.MinS, r.CountS); st.MaxS = Math.Max(st.MaxS, r.CountS); sumS += r.CountS; + if (r.CountN >= TargetNorth && r.CountS >= TargetSouth) st.MeetBoth++; + if (r.CountS >= r.CountN) st.SouthAtLeastNorth++; + st.Specks += r.SpecksN + r.SpecksS; st.Clusters += r.ClustersN + r.ClustersS; st.Blobs += r.BlobsN + r.BlobsS; + meds.Add(r.SizeMed); st.SizeMax = Math.Max(st.SizeMax, r.SizeMax); + } + if (st.Seeds == 0) { st.MinN = st.MinS = 0; } + else { st.MeanN = sumN / st.Seeds; st.MeanS = sumS / st.Seeds; meds.Sort(); st.SizeMed = meds[meds.Count / 2]; } + outp.Add(st); + } + return outp; + } + + /// The table seed whose SOUTH count at the preset level is lowest (ties → lower total, then first in the pool), excluding the plate seed. + private static int PickBulgeSeed(List rows, string midLabel, int plateSeed) + { + int best = 0, bestS = int.MaxValue, bestTotal = int.MaxValue; + foreach (var r in rows) + { + if (r.Level != midLabel || r.Seed == plateSeed) continue; + int total = r.CountN + r.CountS; + if (r.CountS < bestS || (r.CountS == bestS && total < bestTotal)) { best = r.Seed; bestS = r.CountS; bestTotal = total; } + } + return best == 0 ? plateSeed : best; + } + + /// Copy the pass's guard ledger into a table row (the pass reports it; the tool does not recompute it). + private static void ReadGuardLedger(Pass1Result p1, Row row) + { + var l = p1.OffshoreLedger; + if (l == null) return; + row.PreN = l.PreGuardNorth; row.PreS = l.PreGuardSouth; + row.SpecksN = l.SpecksNorth; row.SpecksS = l.SpecksSouth; + row.ClustersN = l.ClustersNorth; row.ClustersS = l.ClustersSouth; + row.BlobsN = l.BlobsNorth; row.BlobsS = l.BlobsSouth; + } + + // ---- the curve, measured exactly as tasks 03/04/05 did ---------------- + + private static (CurveKnots, ClimbCalibration, Dictionary) CalibrateCurve(int calibSize, float sea, CurveAnchors anchors) { var rawPool = new LandHistogram(sea); var pass1 = new Dictionary(); @@ -284,7 +429,7 @@ namespace IslaApocalypse.Tools var cal = ClimbCalibration.FromPercentiles(pcts, rawQ, outQ, ceilingRaw, HeightCurve.EffectiveSpikeMax(pass1[CalibrationSeeds[0]].HMaxSeed, knots, anchors), anchors.RedCeil, anchors.PeakCap, mountainLift: 1.0f, peakSharpness: 1.0f); - return (knots, cal); + return (knots, cal, pass1); } private static TerrainGenConfig BaseConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a, @@ -298,7 +443,7 @@ namespace IslaApocalypse.Tools // ---- output ----------------------------------------------------------- - private static void WriteVariant(string batchRoot, Pass1Result p1, Pass2Result p2, float sea, + private static void WritePlate(string batchRoot, Pass1Result p1, Pass2Result p2, float sea, CurveAnchors anchors, bool skipRaw, int countN, int countS) { string dir = Path.Combine(batchRoot, $"{p2.Seed}_{p2.VariantLabel}"); @@ -319,51 +464,154 @@ namespace IslaApocalypse.Tools // ⭐ The tag overlay — the one artifact that shows the DATA this pass set. TagOverlayRenderer.SavePng(p2.Height, p2.IsOffshoreIsland, p2.IslandHemisphere, p2.MapSize, sea, - p1.OffshoreCentres, countN, countS, Path.Combine(dir, "tags.png")); + countN, countS, Path.Combine(dir, "tags.png")); } - private static void WriteIndex(string batchRoot, int mapSize, int calibSize, int primary, - int[] hybridSeeds, int[] smallSeeds, List rows, Dictionary knobs, - List hard, List perVariant, bool allOk) + private static string CountTableMarkdown(List levels, List rows, List stats) { var sb = new StringBuilder(); - sb.AppendLine("# Batch 05 — offshore islands: the faithful rejoin, then the reshape"); + sb.AppendLine("| Level | Seed | **N** | **S** | total | pre-guard N / S | specks | clusters | blobs | size cells min / median / mean / max | lifted cells | HMaxSeed before → after | oracle | ms |"); + sb.AppendLine("|---|---|---|---|---|---|---|---|---|---|---|---|---|---|"); + foreach (var lv in levels) + foreach (var r in rows) + { + if (r.Level != lv.Label) continue; + sb.AppendLine($"| `{r.Level}` | `{r.Seed}` | **{r.CountN}** | **{r.CountS}** | {r.CountN + r.CountS} | {r.PreN} / {r.PreS} | " + + $"{r.SpecksN + r.SpecksS} | {r.ClustersN + r.ClustersS} | {r.BlobsN + r.BlobsS} | " + + $"{r.SizeMin} / {r.SizeMed} / {r.SizeMean:F0} / {r.SizeMax} | {r.Lifted:N0} | " + + $"{r.HMaxBefore:F4} → {r.HMaxAfter:F4}{(r.HMaxBefore != r.HMaxAfter ? " ⚠" : "")} | {(r.Ok ? "pass" : "**FAIL**")} | {r.Ms} |"); + } sb.AppendLine(); - sb.AppendLine("Stage 1 rejoins the reference's coast shelf + islets verbatim (`faithful`, the control)."); - sb.AppendLine("Stage 2 reshapes: **small / low / flat / rigid**, a **seeded floor of ≥2 N / ≥4 S** islands"); - sb.AppendLine("(min-separated, positions varying per seed, never fixed zones) plus **organic extras weighted"); - sb.AppendLine("south**, corners and the outer edge allowed. Every tagged island cell carries a hemisphere."); + sb.AppendLine("**Per level — the consistency read (min / mean / max over the seeds):**"); sb.AppendLine(); + sb.AppendLine($"| Level | density N / S | seeds | **N min / mean / max** | **S min / mean / max** | seeds with N ≥ {TargetNorth} & S ≥ {TargetSouth} | seeds with S ≥ N | specks / clusters / blobs reverted | median island (cells) | largest island (cells) |"); + sb.AppendLine("|---|---|---|---|---|---|---|---|---|---|"); + foreach (var st in stats) + { + var lv = levels.Find(l => l.Label == st.Label); + sb.AppendLine($"| `{st.Label}` | {lv.Settings.Density:F4} / {lv.Settings.DensitySouth:F4} | {st.Seeds} | **{st.MinN} / {st.MeanN:F1} / {st.MaxN}** | **{st.MinS} / {st.MeanS:F1} / {st.MaxS}** | " + + $"**{st.MeetBoth} / {st.Seeds}** | {st.SouthAtLeastNorth} / {st.Seeds} | {st.Specks} / {st.Clusters} / {st.Blobs} | {st.SizeMed} | {st.SizeMax} |"); + } + return sb.ToString(); + } + + private static void WriteCountTable(string batchRoot, int tableSize, List levels, List rows, List stats) + { + var sb = new StringBuilder(); + sb.AppendLine($"# The count table — {rows.Count / Math.Max(1, levels.Count)} seeds × {levels.Count} density levels at {tableSize}"); + sb.AppendLine(); + sb.AppendLine("Tagged 8-connected island components per hemisphere (centroid), after the guards. **No count is forced**;"); + sb.AppendLine("this is the statistical outcome of the tuning. NORTH = rows [0, N/2), SOUTH = rows [N/2, N); y runs south."); + sb.AppendLine(); + sb.Append(CountTableMarkdown(levels, rows, stats)); + WriteText(Path.Combine(batchRoot, "count_table.md"), sb.ToString()); + + var csv = new StringBuilder(); + csv.AppendLine("level,seed,density_n,density_s,north,south,total,preguard_n,preguard_s,specks,clusters,blobs,size_min,size_median,size_mean,size_max,lifted_cells,hmax_before,hmax_after,oracle,ms"); + foreach (var r in rows) + { + var lv = levels.Find(l => l.Label == r.Level); + csv.AppendLine(string.Join(",", r.Level, r.Seed, lv.Settings.Density.ToString("F5", System.Globalization.CultureInfo.InvariantCulture), + lv.Settings.DensitySouth.ToString("F5", System.Globalization.CultureInfo.InvariantCulture), + r.CountN, r.CountS, r.CountN + r.CountS, r.PreN, r.PreS, r.SpecksN + r.SpecksS, r.ClustersN + r.ClustersS, r.BlobsN + r.BlobsS, + r.SizeMin, r.SizeMed, r.SizeMean.ToString("F1", System.Globalization.CultureInfo.InvariantCulture), r.SizeMax, r.Lifted, + r.HMaxBefore.ToString("G9", System.Globalization.CultureInfo.InvariantCulture), r.HMaxAfter.ToString("G9", System.Globalization.CultureInfo.InvariantCulture), + r.Ok ? "pass" : "FAIL", r.Ms)); + } + WriteText(Path.Combine(batchRoot, "count_table.csv"), csv.ToString()); + } + + private static string DiagnosisMarkdown(int calibSize, Level mid, List diag, + HemisphereDiagnosis poolN, HemisphereDiagnosis poolS, string interpretation) + { + var sb = new StringBuilder(); + sb.AppendLine($"Measured on the calibration pool at {calibSize} (shelf on, offshore off — the field the islet layer sees), with the"); + sb.AppendLine($"`{mid.Label}` thresholds (density N {mid.Settings.Density:F4} / S {mid.Settings.DensitySouth:F4}). *Zone* = cells with zone weight > 0"); + sb.AppendLine("(pass the falloff test + moat + outer bound); *peaks* = strict 8-neighbour local maxima of the islet noise field on sea cells;"); + sb.AppendLine("*sole blocker* = sea cells that fail exactly one gate (loosen that gate and they join the zone); *lost > thr* = over-threshold"); + sb.AppendLine("peaks outside the zone, with the gate(s) that excluded them."); + sb.AppendLine(); + sb.AppendLine(OffshoreDiagnosis.TableHeader()); + foreach (var r in diag) + { + sb.AppendLine(OffshoreDiagnosis.TableRow(r, r.North)); + sb.AppendLine(OffshoreDiagnosis.TableRow(r, r.South)); + } + var poolRep = new OffshoreDiagnosis.Report { Seed = 0 }; + sb.AppendLine(OffshoreDiagnosis.TableRow(poolRep, poolN).Replace("| `0` |", "| **pool** |")); + sb.AppendLine(OffshoreDiagnosis.TableRow(poolRep, poolS).Replace("| `0` |", "| **pool** |")); + sb.AppendLine(); + sb.AppendLine($"**Reading:** {interpretation}"); + return sb.ToString(); + } + + private static void WriteDiagnosis(string batchRoot, int calibSize, Level mid, List diag, + HemisphereDiagnosis poolN, HemisphereDiagnosis poolS, string interpretation) + { + var sb = new StringBuilder(); + sb.AppendLine("# The south diagnosis — zone area, gates and peaks per hemisphere"); + sb.AppendLine(); + sb.Append(DiagnosisMarkdown(calibSize, mid, diag, poolN, poolS, interpretation)); + WriteText(Path.Combine(batchRoot, "diagnosis.md"), sb.ToString()); + } + + private static void WriteIndex(string batchRoot, int mapSize, int tableSize, int calibSize, int plateSeed, int bulgeSeed, + int[] tableSeeds, List levels, List rows, List stats, List plateRows, + List diag, HemisphereDiagnosis poolN, HemisphereDiagnosis poolS, string interpretation, + List hard, List perField, bool allOk, bool tableOnly) + { + var mid = levels[1]; + var sb = new StringBuilder(); + sb.AppendLine("# Batch 06 — offshore islands: organic-only, tuned for coverage, south-weighted, no forced count"); + sb.AppendLine(); + sb.AppendLine("The chat2/05 forced floor (seeded stamps, guaranteed ≥2 N / ≥4 S) is **reverted out** — it looked stamped."); + sb.AppendLine("The **organic noise-field layer is the only island mechanism**; this batch tunes its **density** (the main"); + sb.AppendLine("knob) and a **south weight** so it yields *a few south / a couple north* **consistently across seeds, as a"); + sb.AppendLine("statistical outcome** — never a hard-coded count. Guards against slop: specks, clusters and blobs are"); + sb.AppendLine("reverted whole; every surviving island is a noise outline, small, low, crisp."); + sb.AppendLine(); + if (tableOnly) sb.AppendLine("> ⚠ **ISLA_TABLE_ONLY** — a probe run: diagnosis + count table only, no regressions, no plates. Not the batch of record.\n"); sb.AppendLine("## ⭐ Open this first"); sb.AppendLine(); - sb.AppendLine($"1. **`{primary}_hybrid/tags.png`** — the tag overlay: grey mainland, cyan = island N, orange = island S,"); - sb.AppendLine(" rings = the seeded floor. Count, N/S split and tag correctness in one glance."); - sb.AppendLine($"2. **`{primary}_hybrid/relief.png`** beside **`{primary}_faithful/relief.png`** — the reshape vs the reference."); - sb.AppendLine($"3. Then the other hybrid seeds' `tags.png` — the floor holds everywhere; the extras vary."); + sb.AppendLine($"1. **`{plateSeed}_density_mid/tags.png`** — the preset of record: grey mainland, cyan = island N, orange = island S."); + sb.AppendLine(" No rings any more — nothing is seeded. Then its `relief.png` for the shape."); + sb.AppendLine($"2. **`{plateSeed}_density_low/`** and **`{plateSeed}_density_high/`** beside it — the same seed, less and more density;"); + sb.AppendLine(" pick the look by eye (more islands vs slop)."); + sb.AppendLine($"3. **`{bulgeSeed}_density_mid/tags.png`** — the preset on the southern-bulge seed (the table seed whose south was"); + sb.AppendLine(" sparsest at `density_mid`): the south tuning is not seed-specific."); + sb.AppendLine("4. Then the count table below — *does it consistently give a few south / a couple north?*"); sb.AppendLine(); sb.AppendLine($"**Hemisphere convention (from the code, not invented):** y runs SOUTH. NORTH = rows `[0, {mapSize / 2})`,"); - sb.AppendLine($"SOUTH = rows `[{mapSize / 2}, {mapSize})`. The spine fades southward; the southern sinker bites the bottom 25 %;"); - sb.AppendLine("snow-town north, shipwreck south. Component hemisphere is by centroid; the tag per cell is by row."); + sb.AppendLine($"SOUTH = rows `[{mapSize / 2}, {mapSize})`. Component hemisphere is by centroid; the tag per cell is by row."); sb.AppendLine(); - sb.AppendLine("## ⭐ The count table — the floor, and the spread above it"); + sb.AppendLine("## The four plates"); sb.AppendLine(); - sb.AppendLine("| Variant | Seed | N | S | total | lifted cells | HMaxSeed before → after | oracle | seeded floor centres (x,y) |"); - sb.AppendLine("|---|---|---|---|---|---|---|---|---|"); - foreach (var r in rows) - sb.AppendLine($"| `{r.Variant}` | `{r.Seed}` | **{r.CountN}** | **{r.CountS}** | {r.CountN + r.CountS} | {r.Lifted:N0} | " + - $"{r.HMaxBefore:F4} → {r.HMaxAfter:F4}{(r.HMaxBefore != r.HMaxAfter ? " ⚠" : "")} | {(r.Ok ? "pass" : "**FAIL**")} | {r.Centres} |"); + sb.AppendLine("| Plate | N | S | total | pre-guard N / S | specks / clusters / blobs | lifted cells | oracle |"); + sb.AppendLine("|---|---|---|---|---|---|---|---|"); + foreach (var r in plateRows) + sb.AppendLine($"| `{r.Seed}_{r.Level}/` | **{r.CountN}** | **{r.CountS}** | {r.CountN + r.CountS} | {r.PreN} / {r.PreS} | {r.SpecksN + r.SpecksS} / {r.ClustersN + r.ClustersS} / {r.BlobsN + r.BlobsS} | {r.Lifted:N0} | {(r.Ok ? "pass" : "**FAIL**")} |"); + if (plateRows.Count == 0) sb.AppendLine("| *(no plates — probe run)* | | | | | | | |"); sb.AppendLine(); - sb.AppendLine("`floor_only` / `hybrid` / `dense` carry a floor of **≥2 N / ≥4 S**; `faithful` has no floor (the"); - sb.AppendLine("reference's probabilistic layer) and is the control."); + sb.AppendLine($"## ⭐ The count table — {tableSeeds.Length} seeds × 3 levels at {tableSize} (the consistency evidence)"); sb.AppendLine(); - sb.AppendLine("## The knobs per variant"); + sb.Append(CountTableMarkdown(levels, rows, stats)); sb.AppendLine(); - sb.AppendLine("| Variant | settings |"); + sb.AppendLine("Also as plain data: `count_table.md` / `count_table.csv`."); + sb.AppendLine(); + sb.AppendLine("## The levels — density and south weight"); + sb.AppendLine(); + sb.AppendLine("| Level | settings |"); sb.AppendLine("|---|---|"); - foreach (var kv in knobs) sb.AppendLine($"| `{kv.Key}` | {kv.Value} |"); + foreach (var lv in levels) sb.AppendLine($"| `{lv.Label}`{(lv == mid ? " ⭐ preset of record" : "")} | {lv.Settings.Describe()} |"); sb.AppendLine(); - sb.AppendLine("The coast shelf is ON for every variant (strength 0.775, scale 100 m, BitDecrement-clamped). It is"); - sb.AppendLine("invisible on these hypsometric plates — ported faithfully, judged when water renders."); + sb.AppendLine("The coast shelf is ON for every field (strength 0.775, scale 100 m, BitDecrement-clamped); invisible on these"); + sb.AppendLine("hypsometric plates — ported faithfully, judged when water renders. Moat / falloff test / outer bound unchanged from chat2/05."); + sb.AppendLine(); + sb.AppendLine("## The south diagnosis — measured before tuning"); + sb.AppendLine(); + sb.Append(DiagnosisMarkdown(calibSize, mid, diag, poolN, poolS, interpretation)); + sb.AppendLine(); + sb.AppendLine("Also as `diagnosis.md`."); sb.AppendLine(); sb.AppendLine("## ⚠ The palette is PROVISIONAL"); sb.AppendLine(); @@ -371,29 +619,32 @@ namespace IslaApocalypse.Tools sb.AppendLine(); sb.AppendLine("## The oracle"); sb.AppendLine(); - sb.AppendLine("Regressions at the calibration size:"); + sb.AppendLine("Regressions (offshore OFF must be bit-identical to Phase 1, task 03 and the `terrain-curve-v1` gallery dump):"); sb.AppendLine(); - sb.AppendLine(ShapingOracle.ToMarkdownTable(hard)); - sb.AppendLine("Per variant × seed (floor h · moat i · mainland unmoved j · tag/coastline k · HMaxSeed l · classify b):"); + sb.AppendLine(hard.Count == 0 ? "*(skipped — probe run)*\n" : ShapingOracle.ToMarkdownTable(hard)); + sb.AppendLine("Per field (moat i · mainland unmoved j · tag/coastline k · HMaxSeed l · classify b):"); sb.AppendLine(); - sb.AppendLine(ShapingOracle.ToMarkdownTable(perVariant)); + sb.AppendLine(ShapingOracle.ToMarkdownTable(perField)); sb.AppendLine($"**{(allOk ? "ALL HARD CHECKS PASS" : "⚠⚠ FAILURES — do not judge this batch")}**"); sb.AppendLine(); sb.AppendLine("## Disposability"); sb.AppendLine(); sb.AppendLine("| Artifact | Keep? |"); sb.AppendLine("|---|---|"); - sb.AppendLine("| `tags.png`, `relief.png`, `INDEX.md` | **keep** |"); + sb.AppendLine("| `tags.png`, `relief.png`, `INDEX.md`, `count_table.md` / `.csv`, `diagnosis.md` | **keep** |"); sb.AppendLine("| `grayscale.png` | ♻ regenerable from the `.f32` |"); sb.AppendLine("| `height.f32` | ♻ regenerable from seed + code — large, clear freely |"); sb.AppendLine("| `scratch/` | persistent by rule; never cleaned |"); sb.AppendLine(); - sb.AppendLine($"MapSize {mapSize}, curve calibrated at {calibSize} with offshore off. {WorldScale.Describe()}."); + sb.AppendLine($"Plates at {mapSize}, table at {tableSize}, curve calibrated at {calibSize} with offshore off. {WorldScale.Describe()}."); + WriteText(Path.Combine(batchRoot, "INDEX.md"), sb.ToString()); + } - string index = Path.Combine(batchRoot, "INDEX.md"); - using var f = Godot.FileAccess.Open(index, Godot.FileAccess.ModeFlags.Write); - if (f == null) { GD.PrintErr($"could not write {index}"); return; } - f.StoreString(sb.ToString()); + private static void WriteText(string path, string text) + { + using var f = Godot.FileAccess.Open(path, Godot.FileAccess.ModeFlags.Write); + if (f == null) { GD.PrintErr($"could not write {path}"); return; } + f.StoreString(text); } // ---- env helpers -------------------------------------------------------- diff --git a/Tools/Scripts/OffshorePass.cs b/Tools/Scripts/OffshorePass.cs index 7d95c0b..3480d7c 100644 --- a/Tools/Scripts/OffshorePass.cs +++ b/Tools/Scripts/OffshorePass.cs @@ -6,8 +6,9 @@ using IslaApocalypse.Core; namespace IslaApocalypse.Tools { /// - /// ⭐⭐ PASS 1b — THE COAST SHELF AND THE OFFSHORE ISLETS (chat2/05). Runs over the finished - /// pass-1 arrays, IN PLACE, before HMaxSeed is taken and before anything classifies. + /// ⭐⭐ PASS 1b — THE COAST SHELF AND THE OFFSHORE ISLETS (chat2/05, retuned chat2/06). Runs over + /// the finished pass-1 arrays, IN PLACE, before HMaxSeed is taken and before anything + /// classifies. /// /// ═══ WHERE THIS SITS, AND WHY IT IS A SECOND SWEEP ═══ /// @@ -16,30 +17,30 @@ namespace IslaApocalypse.Tools /// sweep over the arrays the first loop produced. Per pixel the inputs are identical — the raw /// height, the sea level, the pre-Trench falloff, (x, y) — and the operations are applied in the /// same order on the same floats, so the FAITHFUL mode reproduces the reference cell for cell. - /// What the second sweep buys is the seeded floor: choosing island centres needs the whole - /// depth/falloff field to exist first, which a single pixel loop cannot provide. + /// The second sweep is also what lets the slop guards see whole islands: a component rule needs + /// the finished field. /// /// ⚠⚠ THE ORDERING THAT CLOSES chat2/00 DRIFT §2: the caller recomputes HMaxSeed AFTER this /// pass, as the reference did, so the curve's per-seed peak normalization sees the same maximum /// the reference saw. Expected to be unchanged (a ~34 m crest is far below any peak) — reported, /// not assumed. /// - /// ═══ THE THREE SUB-PASSES ═══ + /// ═══ THE SUB-PASSES ═══ /// /// 1. SHELF every below-sea cell; depth-preserving; held strictly below sea by BitDecrement. - /// 2. ORGANIC the reference's noise layer — faithful, or reshaped (smaller / lower / flatter / - /// crisper, south-weighted) — every below-sea cell inside the zone mask. - /// 3. FLOOR (Hybrid only) a seed-derived RNG places ≥N north / ≥S south stamps, each - /// min-separated from the others AND clear of ALL existing land by a gap, so each - /// stamp is its own connected component BY CONSTRUCTION. Positions vary per seed; - /// there are no reserved zones. Refuses loudly if the floor cannot be placed. + /// 2. ORGANIC ⭐ THE ONE ISLAND MECHANISM — the reference's noise layer, faithful or reshaped + /// (smaller / lower / flatter / crisper, density + south weight), every below-sea + /// cell inside the zone mask. Nothing places an island from a centre; nothing + /// guarantees a count. (chat2/05's seeded floor was reverted out in chat2/06 — it + /// looked stamped. It lives in git history.) + /// 3. GUARDS (Organic only) specks, clusters and blobs reverted BY COMPONENT, then the + /// reference's submerged humps outside any surviving island's skirt. /// - /// ═══ THE TWO PROTECTIONS ARE APPLIED TO EVERYTHING, INCLUDING THE FLOOR ═══ + /// ═══ THE TWO PROTECTIONS ═══ /// /// The moat (min depth) and the "actually offshore" test (pre-Trench falloff) gate the organic - /// layer per pixel — that is the reference. They ALSO gate every seeded stamp: a centre must sit - /// fully inside the zone (weight exactly 1), and the stamp's every pixel is still multiplied by - /// the zone weight. A guaranteed island cannot be guaranteed onto the mainland or into a lake. + /// layer per pixel — that is the reference. Nothing in this pass can raise a cell outside the + /// zone, so no island can bridge to shore or appear in a lake / the crater bay. /// /// ═══ ⚠ THE ONE HONEST COUPLING ═══ /// @@ -58,15 +59,32 @@ namespace IslaApocalypse.Tools { public bool[,] Tag; // null when the islet layer is off (shelf only) public byte[,] Hemi; - public long ShelfCells, LiftedOrganic, LiftedSeeded; + public long ShelfCells, LiftedOrganic; public float ThresholdNorth = float.NaN, ThresholdSouth = float.NaN; - public List<(int x, int y, int r)> Centres = new(); public List Notes = new(); public int CountNorth, CountSouth, Bridged; - public long LiftedReverted; // specks the guard put back - public long RaisedReverted; // submerged debris bumps the guard put back + + // ---- the guards' ledger (Organic only) ---- + public int PreGuardNorth, PreGuardSouth; // islands before any guard + public int SpecksNorth, SpecksSouth; // reverted as specks + public int ClustersNorth, ClustersSouth; // reverted as too-close-to-a-larger-island + public int BlobsNorth, BlobsSouth; // reverted as oversize + public long LiftedReverted; // surfaced cells the guards put back + public long RaisedReverted; // submerged bump cells the guard put back + internal List<(int x, int y, float h0)> LiftedOrigin = new(); // every SURFACED lift - internal List<(int x, int y, float h0)> RaisedOrigin = new(); // every organic raise, surfaced or not (guard on) + internal List<(int x, int y, float h0)> RaisedOrigin = new(); // every organic raise, surfaced or not (guards on) + + public OffshoreLedger ToLedger() => Tag == null ? null : new OffshoreLedger + { + ThresholdNorth = ThresholdNorth, ThresholdSouth = ThresholdSouth, + PreGuardNorth = PreGuardNorth, PreGuardSouth = PreGuardSouth, + SpecksNorth = SpecksNorth, SpecksSouth = SpecksSouth, + ClustersNorth = ClustersNorth, ClustersSouth = ClustersSouth, + BlobsNorth = BlobsNorth, BlobsSouth = BlobsSouth, + LiftedOrganic = LiftedOrganic, LiftedReverted = LiftedReverted, RaisedReverted = RaisedReverted, + CountNorth = CountNorth, CountSouth = CountSouth, + }; } /// @@ -121,12 +139,10 @@ namespace IslaApocalypse.Tools r.Hemi = new byte[mapSize, mapSize]; bool faithful = s.Mode == OffshoreMode.Faithful; - bool guardOn = !faithful && s.MinIslandAreaFrac > 0f; + bool guardsOn = !faithful && (s.MinIslandAreaFrac > 0f || s.MinSeparationFrac > 0f || s.MaxIslandAreaFrac > 0f); float crest = sea + WorldScale.RawFromMetres(s.CrestM); // seaHere + OFFSHORE_ISLAND_H_M / 251f - // ═══ 2. THE ORGANIC LAYER ═══ - var liftedOrigin = r.LiftedOrigin; // every lift, for the debris guard - if (s.Organic) + // ═══ 2. THE ORGANIC LAYER — the one island mechanism ═══ { FastNoiseLite noise = TerrainNoise.CreateModulation(seed, s.SeedOffset, s.FreqPerMapWidth, scale); @@ -134,14 +150,8 @@ namespace IslaApocalypse.Tools // sample on a stride grid and take the quantile. Deterministic from the seed, and it // makes the density dial mean what it says whatever FastNoiseLite's range turns out // to be. Verbatim: stride 8, (side)² samples, (noise + 1) * 0.5. - const int stride = 8; - int side = mapSize / stride; - var samples = new float[side * side]; - for (int i = 0; i < side; i++) - for (int j = 0; j < side; j++) - samples[i * side + j] = (noise.GetNoise2D(i * stride, j * stride) + 1f) * 0.5f; - - float thrN = IslandFalloff.CalibrateThreshold(samples, s.DensityNorth); + float[] samples = CalibrationSamples(noise, mapSize); + float thrN = IslandFalloff.CalibrateThreshold(samples, s.Density); float thrS = faithful ? thrN : IslandFalloff.CalibrateThreshold(samples, s.DensitySouth); r.ThresholdNorth = thrN; r.ThresholdSouth = thrS; @@ -167,14 +177,7 @@ namespace IslaApocalypse.Tools // The threshold: one number in faithful mode; north/south blended smoothly // across the midline in the reshape, so a straddling island is not sliced. - float thr; - if (faithful) thr = thrN; - else - { - float t = Math.Clamp((y - mid) / band * 0.5f + 0.5f, 0f, 1f); - t = t * t * (3f - 2f * t); - thr = thrN + (thrS - thrN) * t; - } + float thr = faithful ? thrN : BlendedThreshold(y, mid, band, thrN, thrS); float blob = (faithful ? IslandFalloff.OffshoreBlob(v, thr) @@ -185,14 +188,14 @@ namespace IslaApocalypse.Tools float before = h; h = h + (crest - h) * blob; // Mathf.Lerp, written out height[x, y] = h; - if (guardOn) r.RaisedOrigin.Add((x, y, before)); // for the debris guard (reshape only) + if (guardsOn) r.RaisedOrigin.Add((x, y, before)); // for the submerged-bump guard if (before < sea && h >= sea) { r.LiftedOrganic++; r.Tag[x, y] = true; r.Hemi[x, y] = OffshoreAnalysis.HemisphereOfRow(y, mapSize); - liftedOrigin.Add((x, y, before)); + r.LiftedOrigin.Add((x, y, before)); } } } @@ -202,159 +205,153 @@ namespace IslaApocalypse.Tools $"(range {Min(samples):F3}..{Max(samples):F3}); lifted {r.LiftedOrganic:N0} cells above sea."); } - // ═══ 3. THE SEEDED FLOOR (Hybrid only) ═══ - if (s.Mode == OffshoreMode.Hybrid && (s.FloorNorth > 0 || s.FloorSouth > 0)) + // ═══ 3. THE SLOP GUARDS — reshape only; over everything the pass raised ═══ + // + // Four kinds of slop, three component rules and one location rule: + // (a) SPECKS a local maximum that barely clears the threshold surfaces a cap of a few + // cells — not an island. Reverted by MEMBERSHIP (a speck inside a real + // island's skirt is still a speck). + // (b) BLOBS a superlevel region that merged several maxima into one sprawling + // landmass. Reverted by membership. The cap sits well above the natural + // size so it is a net, not a sculptor; how often it bites is reported. + // (c) CLUSTERS two islands whose shores are closer than the minimum separation read as + // one; the SMALLER goes (greedy by size, so the largest of a cluster stays). + // (d) SUBMERGED BUMPS — every blob with any weight lerps the seabed toward the crest + // whether or not it surfaces, so the ocean fills with shallow humps (the + // reference's character; the water pass would draw a reef field nobody + // asked for). Attributed by HUMP: a hump (connected raised region) that + // holds a kept island is that island's own skirt and stays; one that holds + // none goes back to seabed; a dropped island's own cap goes too. + // None of these places, shapes or counts anything. (The faithful control keeps all four; + // that is the reference.) + if (guardsOn && (r.RaisedOrigin.Count > 0 || r.LiftedOrigin.Count > 0)) { - var rng = new Pcg32(seed + s.PlacementSeedOffset); - int radius = Math.Max(2, (int)MathF.Round(s.StampRadiusFrac * mapSize)); - // Rim jitter field: fine enough to put a few lobes around a stamp's rim (wavelength - // ≈ radius / 1.5), stated per map width so the look holds at every size. Seeded off - // the islet offset so it is decorrelated from the organic field but just as deterministic. - FastNoiseLite jitterNoise = s.StampEdgeJitter > 0f - ? TerrainNoise.CreateModulation(seed, s.SeedOffset + 1, 1.5f / MathF.Max(s.StampRadiusFrac, 1e-4f), scale) - : null; - int reachRadius = (int)MathF.Ceiling(radius * (1f + s.StampEdgeJitter)); - int gap = Math.Max(1, (int)MathF.Round(s.LandGapFrac * mapSize)); - float sepPx = s.SeparationFrac * mapSize; - float zoneFloorDepth = s.MinDepthM + s.DepthFeatherM; // zone weight exactly 1 - float zoneFloorFalloff = s.MinFalloff + s.FalloffFeather; - var placed = new List<(int x, int y)>(); + double area = (double)mapSize * mapSize; + long minCells = s.MinIslandAreaFrac > 0f ? Math.Max(1L, (long)Math.Round(s.MinIslandAreaFrac * area)) : 0; + long maxCells = s.MaxIslandAreaFrac > 0f ? Math.Max(1L, (long)Math.Round(s.MaxIslandAreaFrac * area)) : long.MaxValue; + int minSep = s.MinSeparationFrac > 0f ? Math.Max(1, (int)Math.Round(s.MinSeparationFrac * mapSize)) : 0; - foreach (byte hemi in new[] { OffshoreAnalysis.HemiNorth, OffshoreAnalysis.HemiSouth }) - { - int need = hemi == OffshoreAnalysis.HemiNorth ? s.FloorNorth : s.FloorSouth; - if (need <= 0) continue; - string name = OffshoreAnalysis.HemisphereName(hemi); - int y0 = hemi == OffshoreAnalysis.HemiNorth ? 0 : mapSize / 2; - int y1 = hemi == OffshoreAnalysis.HemiNorth ? mapSize / 2 : mapSize; - - int got = 0, attempts = 0; - while (got < need) - { - if (++attempts > s.MaxPlacementAttempts) - throw new InvalidOperationException( - $"[OffshorePass] could not place the seeded floor: {name} needed {need}, placed {got} " + - $"after {attempts - 1} attempts. The valid ocean zone is too small or too crowded " + - $"for sep {s.SeparationFrac:F3} / gap {s.LandGapFrac:F3} / trench ≤ {s.TrenchInner:F2}. " + - "Refusing rather than under-delivering a guaranteed floor."); - - int cx = rng.NextInt(mapSize); - int cy = y0 + rng.NextInt(y1 - y0); - - // Valid ocean: below sea, FULLY inside the zone (moat + falloff + trench), - // so the centre's own weight is exactly 1 and the stamp surfaces. - float hc = height[cx, cy]; - if (hc >= sea) continue; - if (WorldScale.MetresFromRaw(sea - hc) < zoneFloorDepth) continue; - if (preTrench[cx, cy] < zoneFloorFalloff) continue; - float dTr = MathF.Max(MathF.Abs(cx - centerX) / halfSpan, MathF.Abs(cy - centerY) / halfSpan); - if (dTr > s.TrenchInner) continue; - - // Separation from the other seeded centres. - bool tooClose = false; - foreach (var (px, py) in placed) - { - float ddx = px - cx, ddy = py - cy; - if (ddx * ddx + ddy * ddy < sepPx * sepPx) { tooClose = true; break; } - } - if (tooClose) continue; - - // ⭐ THE LAND GAP — the guarantee's mechanism. No land of ANY kind (mainland or - // organic) within radius + gap of the centre, so this stamp can touch nothing - // and is its own connected component by construction. - if (LandWithin(height, mapSize, cx, cy, reachRadius + gap, sea)) continue; - - long lifted = Stamp(height, preTrench, r, cx, cy, radius, s, sea, crest, mapSize, centerX, centerY, halfSpan, jitterNoise); - if (lifted == 0) - throw new InvalidOperationException( - $"[OffshorePass] a seeded stamp at ({cx},{cy}) lifted no cells — the zone test and the stamp disagree. Refusing."); - - placed.Add((cx, cy)); - r.Centres.Add((cx, cy, radius)); - r.LiftedSeeded += lifted; - got++; - r.Notes.Add($"[Offshore] floor {name} #{got} at ({cx},{cy}) r{radius}: lifted {lifted:N0} cells (attempt {attempts})"); - } - } - } - - // ═══ 3b. THE DEBRIS GUARD — reshape only; runs LAST, over everything the pass raised ═══ - // - // Two kinds of debris, one rule. (a) SPECKS: a local maximum that barely clears the - // threshold surfaces a cap of a few cells — not an island. (b) SUBMERGED BUMPS: every - // blob with any weight lerps the seabed toward the crest whether or not it surfaces, so - // the ocean fills with shallow humps — which the first probe plate showed as a field of - // pale speckles across the southern sea, and which the water pass would later draw as a - // reef field nobody asked for. (The faithful control keeps both; that is the reference.) - // - // The rule: find the surviving islands (components at or above the minimum area), take - // each one's bounding box with a margin as its KEEP region, and put every organic raise - // OUTSIDE all keep regions back to the height it had — surfaced or not, tagged or not. A - // real island keeps its own submerged skirt; everything else goes back to seabed. - // - // ⚠ Runs after the floor so a stamp's rim satellite (w·zone crossing the waterline - // non-monotonically) is caught too; the stamp bodies are ~800+ cells and survive by - // construction. The floor is still asserted below. - if (guardOn && (r.RaisedOrigin.Count > 0 || r.LiftedOrigin.Count > 0)) - { - long minCells = Math.Max(1L, (long)Math.Round(s.MinIslandAreaFrac * (double)mapSize * mapSize)); var pre = OffshoreAnalysis.Components(r.Tag, height, sea, mapSize, out int[] compId); + (r.PreGuardNorth, r.PreGuardSouth) = OffshoreAnalysis.CountByHemisphere(pre); - // Rule 1 — SPECKS go by MEMBERSHIP: any surfaced cell whose component is below the - // minimum is reverted and untagged, wherever it sits (a speck inside a real island's - // skirt is still a speck — the first keep-box cut let those survive as 1-cell islands). - // Rule 2 — SUBMERGED BUMPS go by LOCATION: a raise outside every surviving island's - // skirt (bbox + margin) goes back to seabed; inside, it is that island's own slope. - var small = new HashSet(); - var keep = new List<(int x0, int y0, int x1, int y1)>(); + var dropped = new HashSet(); + var byId = new Dictionary(); + foreach (var c in pre) byId[c.Id] = c; + + // (a) specks and (b) blobs — by size. foreach (var c in pre) { - if (c.Cells < minCells) { small.Add(c.Id); continue; } - int w = c.MaxX - c.MinX + 1, hgt = c.MaxY - c.MinY + 1; - int margin = Math.Max(4, Math.Max(w, hgt) / 2); - keep.Add((c.MinX - margin, c.MinY - margin, c.MaxX + margin, c.MaxY + margin)); + if (c.Cells < minCells) { dropped.Add(c.Id); Bump(r, c.Hemisphere, ref r.SpecksNorth, ref r.SpecksSouth); } + else if (c.Cells > maxCells) { dropped.Add(c.Id); Bump(r, c.Hemisphere, ref r.BlobsNorth, ref r.BlobsSouth); } } - bool Kept(int x, int y) + // (c) clusters — greedy by size among the survivors of (a)/(b). + if (minSep > 0) { - foreach (var (x0, y0, x1, y1) in keep) + var survivors = new List(); + foreach (var c in pre) if (!dropped.Contains(c.Id)) survivors.Add(c); + survivors.Sort((a, b) => b.Cells.CompareTo(a.Cells)); // largest first + var boundary = OffshoreAnalysis.BoundaryCells(r.Tag, compId, mapSize, r.LiftedOrigin); + var kept = new List(); + foreach (var c in survivors) + { + bool tooClose = false; + foreach (var k in kept) + { + if (OffshoreAnalysis.BoxGap(c, k) >= minSep) continue; // cannot be closer than the bbox gap + if (OffshoreAnalysis.MinChebyshev(boundary[c.Id], boundary[k.Id], minSep) < minSep) { tooClose = true; break; } + } + if (tooClose) { dropped.Add(c.Id); Bump(r, c.Hemisphere, ref r.ClustersNorth, ref r.ClustersSouth); } + else kept.Add(c); + } + } + + // (d) THE SUBMERGED BUMPS — by HUMP, not by box. A hump is one 8-connected region of + // raised cells (the blob's footprint, surfaced or not). A hump that holds a kept island + // is that island's own skirt and stays whole; a hump that holds none is a reef nobody + // asked for and goes back to seabed whole. A DROPPED island's own cap — its bbox plus a + // margin — is reverted even inside a kept hump, or its rim would stay as a hollow ring + // beside its neighbour (the first probe plate showed exactly those ghost outlines). + var raised = new bool[mapSize, mapSize]; + foreach (var (x, y, _) in r.RaisedOrigin) raised[x, y] = true; + var humpId = new int[mapSize * mapSize]; + var humpKept = new List { false }; // index 0 unused + { + var stack = new Stack(); + foreach (var (sx, sy, _) in r.RaisedOrigin) + { + if (humpId[sx * mapSize + sy] != 0) continue; + int id = humpKept.Count; humpKept.Add(false); + humpId[sx * mapSize + sy] = id; stack.Push(sx * mapSize + sy); + bool kept = false; + while (stack.Count > 0) + { + int cur = stack.Pop(); int cx = cur / mapSize, cy = cur % mapSize; + if (r.Tag[cx, cy] && !dropped.Contains(compId[cur])) kept = true; + for (int dx = -1; dx <= 1; dx++) + { + int nx = cx + dx; if (nx < 0 || nx >= mapSize) continue; + for (int dy = -1; dy <= 1; dy++) + { + int ny = cy + dy; if (ny < 0 || ny >= mapSize || (dx == 0 && dy == 0)) continue; + if (!raised[nx, ny]) continue; + int ni = nx * mapSize + ny; + if (humpId[ni] != 0) continue; + humpId[ni] = id; stack.Push(ni); + } + } + } + humpKept[id] = kept; + } + } + var dropBoxes = new List<(int x0, int y0, int x1, int y1)>(); + foreach (var c in pre) + { + if (!dropped.Contains(c.Id)) continue; + int w = c.MaxX - c.MinX + 1, hgt = c.MaxY - c.MinY + 1; + int margin = Math.Max(4, Math.Max(w, hgt) / 2); + dropBoxes.Add((c.MinX - margin, c.MinY - margin, c.MaxX + margin, c.MaxY + margin)); + } + bool InDropBox(int x, int y) + { + foreach (var (x0, y0, x1, y1) in dropBoxes) if (x >= x0 && x <= x1 && y >= y0 && y <= y1) return true; return false; } - long revertedRaised = 0, revertedLifted = 0; + long revertedLifted = 0, revertedRaised = 0; foreach (var (x, y, h0) in r.LiftedOrigin) { - if (!small.Contains(compId[x * mapSize + y])) continue; + if (!dropped.Contains(compId[x * mapSize + y])) continue; height[x, y] = h0; r.Tag[x, y] = false; r.Hemi[x, y] = OffshoreAnalysis.HemiNone; revertedLifted++; } foreach (var (x, y, h0) in r.RaisedOrigin) { if (r.Tag[x, y]) continue; // a kept island's own surfaced cell - if (Kept(x, y)) continue; // inside a kept island's skirt + bool keepIt = humpKept[humpId[x * mapSize + y]] && !InDropBox(x, y); + if (keepIt) continue; if (height[x, y] > h0) { height[x, y] = h0; revertedRaised++; } } r.LiftedReverted = revertedLifted; r.RaisedReverted = revertedRaised; - r.Notes.Add($"[Offshore] debris guard: {small.Count} of {pre.Count} islands below {minCells:N0} cells reverted " + - $"({revertedLifted:N0} surfaced cells), plus {revertedRaised:N0} submerged bump cells outside any kept island's skirt."); + r.Notes.Add($"[Offshore] guards: of {pre.Count} islands (N {r.PreGuardNorth} / S {r.PreGuardSouth}) reverted " + + $"{r.SpecksNorth + r.SpecksSouth} specks (< {minCells:N0} cells), " + + $"{r.ClustersNorth + r.ClustersSouth} clustered (< {minSep} px from a larger island), " + + $"{r.BlobsNorth + r.BlobsSouth} blobs (> {(maxCells == long.MaxValue ? "∞" : maxCells.ToString("N0"))} cells) " + + $"— {revertedLifted:N0} surfaced cells, plus {revertedRaised:N0} submerged bump cells outside any kept island's hump."); } - // ═══ PROVE THE FLOOR ON THE FINISHED FIELD — in the pass, before anyone looks ═══ + // ═══ PROVE THE MOAT ON THE FINISHED FIELD — in the pass, before anyone looks ═══ var comps = OffshoreAnalysis.Components(r.Tag, height, sea, mapSize); (r.CountNorth, r.CountSouth) = OffshoreAnalysis.CountByHemisphere(comps); r.Bridged = OffshoreAnalysis.BridgedCount(comps); var (szMin, szMed, szMean, szMax, _) = OffshoreAnalysis.SizeSummary(comps, 0); r.Notes.Add($"[Offshore] islands: {r.CountNorth} north, {r.CountSouth} south ({comps.Count} components, " + - $"{r.Bridged} bridged to mainland); lifted {r.LiftedOrganic + r.LiftedSeeded - r.LiftedReverted:N0} cells net" + + $"{r.Bridged} bridged to mainland); lifted {r.LiftedOrganic - r.LiftedReverted:N0} cells net" + $"{(r.RaisedReverted > 0 ? $", {r.RaisedReverted:N0} submerged debris cells reverted" : "")}; " + $"size cells min {szMin} median {szMed} mean {szMean:F0} max {szMax}."); - if (s.Mode == OffshoreMode.Hybrid && (r.CountNorth < s.FloorNorth || r.CountSouth < s.FloorSouth)) - throw new InvalidOperationException( - $"[OffshorePass] FLOOR VIOLATION after placement: {r.CountNorth} N / {r.CountSouth} S against " + - $"{s.FloorNorth} N / {s.FloorSouth} S. The land gap should have made this impossible. Refusing."); if (r.Bridged > 0) throw new InvalidOperationException( $"[OffshorePass] MOAT VIOLATION: {r.Bridged} island(s) touch mainland land. Refusing."); @@ -362,93 +359,46 @@ namespace IslaApocalypse.Tools return r; } - /// One seeded stamp: a flat-topped disc lerped toward the crest, zone-masked per pixel. - private static long Stamp(float[,] height, float[,] preTrench, Result r, int cx, int cy, int radius, - OffshoreSettings s, float sea, float crest, int mapSize, float centerX, float centerY, float halfSpan, - FastNoiseLite jitterNoise) + /// The reference's calibration sample: stride 8, (side)² samples, (noise + 1) · 0.5. Shared with the diagnosis. + public static float[] CalibrationSamples(FastNoiseLite noise, int mapSize) { - long lifted = 0; - int reach = (int)MathF.Ceiling(radius * (1f + s.StampEdgeJitter)); - for (int dx = -reach; dx <= reach; dx++) - { - int px = cx + dx; - if (px < 0 || px >= mapSize) continue; - for (int dy = -reach; dy <= reach; dy++) - { - int py = cy + dy; - if (py < 0 || py >= mapSize) continue; - - // The rim wanders: the effective radius at this pixel is the nominal one scaled - // by ±jitter from a fine noise field. Flat top and crisp shore are kept; the - // compass-drawn outline is not. - float rEff = radius; - if (jitterNoise != null) - rEff = radius * (1f + s.StampEdgeJitter * jitterNoise.GetNoise2D(px, py)); - - float dist = MathF.Sqrt(dx * dx + dy * dy); - float w = IslandFalloff.StampWeight(dist, rEff, s.StampCoreFrac); - if (w <= 0f) continue; - - float h = height[px, py]; - if (h >= sea) continue; // never touches land - - // ⭐ The moat and the falloff test apply to the FLOOR too. Absolute. - float zone = IslandFalloff.OffshoreZoneWeight( - WorldScale.MetresFromRaw(sea - h), preTrench[px, py], - MathF.Abs(px - centerX) / halfSpan, MathF.Abs(py - centerY) / halfSpan, - s.MinDepthM, s.DepthFeatherM, s.MinFalloff, s.FalloffFeather, - s.TrenchInner, s.TrenchOuter); - float t = w * zone; - if (t <= 0f) continue; - - float before = h; - h = h + (crest - h) * t; - height[px, py] = h; - - if (before < sea && h >= sea) - { - lifted++; - r.Tag[px, py] = true; - r.Hemi[px, py] = OffshoreAnalysis.HemisphereOfRow(py, mapSize); - r.LiftedOrigin.Add((px, py, before)); - } - } - } - return lifted; + const int stride = 8; + int side = mapSize / stride; + var samples = new float[side * side]; + for (int i = 0; i < side; i++) + for (int j = 0; j < side; j++) + samples[i * side + j] = (noise.GetNoise2D(i * stride, j * stride) + 1f) * 0.5f; + return samples; } - /// Any land cell (at/above sea) within a Chebyshev box of ? Conservative by design. - private static bool LandWithin(float[,] height, int mapSize, int cx, int cy, int reach, float sea) + /// The north/south threshold, smoothstep-blended across the midline. Shared with the diagnosis. + public static float BlendedThreshold(int y, float mid, float band, float thrN, float thrS) { - int x0 = Math.Max(0, cx - reach), x1 = Math.Min(mapSize - 1, cx + reach); - int y0 = Math.Max(0, cy - reach), y1 = Math.Min(mapSize - 1, cy + reach); - for (int x = x0; x <= x1; x++) - for (int y = y0; y <= y1; y++) - if (height[x, y] >= sea) return true; - return false; + float t = Math.Clamp((y - mid) / band * 0.5f + 0.5f, 0f, 1f); + t = t * t * (3f - 2f * t); + return thrN + (thrS - thrN) * t; + } + + private static void Bump(Result r, byte hemi, ref int north, ref int south) + { + if (hemi == OffshoreAnalysis.HemiNorth) north++; else south++; } private static float Min(float[] a) { float m = float.MaxValue; foreach (float v in a) if (v < m) m = v; return m; } private static float Max(float[] a) { float m = float.MinValue; foreach (float v in a) if (v > m) m = v; return m; } + } - /// - /// PCG32 (O'Neill) — the same tiny deterministic generator the reference's erosion pass - /// used. Seeded from the world seed + an offset, so island POSITIONS vary per world and - /// reproduce exactly for one. Trivially portable to C++. - /// - private struct Pcg32 - { - private ulong _state; - public Pcg32(int seed) { _state = 0; NextU(); _state += (ulong)(uint)seed; NextU(); } - public uint NextU() - { - ulong old = _state; - _state = old * 6364136223846793005UL + 1442695040888963407UL; - uint xorshifted = (uint)(((old >> 18) ^ old) >> 27); - int rot = (int)(old >> 59); - return (xorshifted >> rot) | (xorshifted << (-rot & 31)); - } - public int NextInt(int n) => (int)(NextU() % (uint)n); - } + /// + /// The islet layer's ledger, carried on Pass1Result for the report: thresholds, the + /// pre-guard island count, what each guard reverted, the final count. Numbers only — the tag + /// arrays are carried separately. + /// + public sealed class OffshoreLedger + { + public float ThresholdNorth, ThresholdSouth; + public int PreGuardNorth, PreGuardSouth; + public int SpecksNorth, SpecksSouth, ClustersNorth, ClustersSouth, BlobsNorth, BlobsSouth; + public long LiftedOrganic, LiftedReverted, RaisedReverted; + public int CountNorth, CountSouth; } } diff --git a/Tools/Scripts/OffshoreSettings.cs b/Tools/Scripts/OffshoreSettings.cs index 8129e43..5e57519 100644 --- a/Tools/Scripts/OffshoreSettings.cs +++ b/Tools/Scripts/OffshoreSettings.cs @@ -11,16 +11,19 @@ namespace IslaApocalypse.Tools /// /// ⭐ chat2/05 stage 1 — the reference's probabilistic layer, verbatim: one noise field, one /// calibrated threshold, the faithful blob, the faithful zone mask. Sparse, no corners, no - /// floor. THE CONTROL. + /// guards. THE CONTROL. /// Faithful, /// - /// ⭐ chat2/05 stage 2 — the loose-guaranteed hybrid: a seeded floor of ≥N north / ≥S south - /// islands (min-separated, positions varying per seed, never fixed zones) PLUS the organic - /// layer on top, reshaped small / low / flat / rigid, south-weighted, corners allowed. + /// ⭐ chat2/06 — THE ONE ISLAND MECHANISM: the organic noise-field layer, reshaped small / + /// low / flat / crisp (chat2/05 stage 2's shape, unchanged), tuned for coverage by DENSITY + /// and a SOUTH WEIGHT, with the speck / separation / blob guards. No seeded floor, no + /// stamps, no count guarantee — every island is a noise outline, and the per-hemisphere + /// counts are a statistical outcome of the tuning (the chat2/05 forced floor was tried and + /// reverted on look; it is in git history, one checkout away). /// - Hybrid, + Organic, } /// @@ -31,19 +34,24 @@ namespace IslaApocalypse.Tools /// ═══ TWO PRESETS, AND WHY BOTH EXIST ═══ /// /// the reference's constants, verbatim. The control in every batch. - /// the reshape — the deliverable. + /// the reshape + the chat2/06 coverage tuning — the deliverable. /// /// The reshape does not EDIT the faithful constants; it sets different values on the same - /// dials. So Faithful() stays bit-reproducible however far the hybrid is tuned. + /// dials. So Faithful() stays bit-reproducible however far the organic layer is tuned. /// /// ═══ THE TWO PROTECTIONS THAT ARE NOT DIALS ═══ /// /// (the moat) and (the "actually offshore" test) /// are exposed here because every knob is, but they are the MAIN-ISLAND AND LAKE PROTECTIONS and /// the presets do not move them. They are what makes "no island can bridge to shore" and "no - /// island in a lake or the crater bay" true by construction — and OffshorePass applies - /// them to the SEEDED stamps as well, so a guaranteed island cannot be guaranteed into the wrong - /// place. + /// island in a lake or the crater bay" true by construction. + /// + /// ═══ ⚠ WHAT IS DELIBERATELY NOT HERE (chat2/06) ═══ + /// + /// No floor, no stamp radius/core/jitter, no separation-of-seeded-centres, no placement RNG — + /// nothing that guarantees a count or places an island from a centre. The forced mechanism + /// (chat2/05 Hybrid()) was reverted out whole; if a hard count ever comes back it is a + /// design decision, not a knob that was left lying around. /// public sealed class OffshoreSettings { @@ -51,9 +59,6 @@ namespace IslaApocalypse.Tools // ---- the organic (noise) layer ---------------------------------------- - /// Run the noise layer at all. Off ⇒ only the seeded floor (the `floor_only` contrast). - public bool Organic = true; - /// Islet noise frequency, periods per map width. Higher ⇒ SMALLER blobs. Reference 14. public float FreqPerMapWidth = IslandFalloff.OFFSHORE_FREQ_ISLANDS; @@ -61,12 +66,24 @@ namespace IslaApocalypse.Tools public int SeedOffset = IslandFalloff.OFFSHORE_SEED_OFFSET; /// - /// Organic density — the fraction of the noise field's ACTUAL sampled distribution that - /// clears the threshold — per hemisphere. Equal ⇒ the reference's single dial. South > - /// north ⇒ the developer's "weighted south". Reference 0.02 / 0.02. + /// ⭐ THE MAIN KNOB — organic density: the fraction of the noise field's ACTUAL sampled + /// distribution that clears the calibrated threshold. More ⇒ more of the field becomes + /// island. This is the NORTH density; the south is this × . + /// Reference 0.02. /// - public float DensityNorth = 0.02f; - public float DensitySouth = 0.02f; + public float Density = 0.02f; + + /// + /// ⭐ THE SOUTH WEIGHT — a per-hemisphere density bias. South density = + /// × this. 1 ⇒ one dial (the reference); > 1 ⇒ "weighted south", + /// the developer's preference. It is applied to the DENSITY (i.e. the calibration quantile), + /// not to the blob shape, so a south island looks exactly like a north island — there are + /// simply more of them. + /// + public float SouthWeight = 1f; + + /// The effective south density. + public float DensitySouth => Density * SouthWeight; /// /// Half-width of the smooth threshold blend across the hemisphere midline, as a fraction of @@ -84,16 +101,37 @@ namespace IslaApocalypse.Tools /// Crest-to-sea edge sharpening exponent. 1 ⇒ the faithful smoothstep. Higher ⇒ crisper shore. public float EdgeSharpness = 1f; + // ---- the slop guards (Organic only; all scale-free fractions of the map) -------------- + // + // More density must not buy speck-debris or merged blobs. Three rules, each a revert BY + // COMPONENT MEMBERSHIP (the island goes back to seabed whole, and is untagged) — none of + // them places, shapes or counts anything. 0 ⇒ that rule is off. The faithful preset has none + // (the reference had no such rules). + /// - /// ⚠ THE DEBRIS GUARD. An organic island smaller than this fraction of the map's area is + /// ⚠ THE SPECK GUARD. An organic island smaller than this fraction of the map's AREA is /// noise debris — a local maximum that barely cleared the threshold and surfaced a cap of a - /// few cells — and is reverted to seabed and untagged. 0 ⇒ off (the reference had no such - /// rule and its "~585 px blobs" comment was about the NOISE wavelength, not the caps). - /// Stated as a fraction so the rule is scale-free: 3e-5 is ~126 cells at 2048, ~2,000 cells - /// (a ~50 m islet) at 8192. + /// few cells — and is reverted to seabed and untagged. 3e-5 is ~126 cells at 2048, ~500 at + /// 4096, ~2,000 cells (a ~50 m islet) at 8192. /// public float MinIslandAreaFrac = 0f; + /// + /// ⚠ THE SEPARATION GUARD — "enough separation that they read as distinct islands". Two + /// surviving islands whose nearest shores are closer than this fraction of the map WIDTH + /// read as one cluster; the SMALLER is reverted. Greedy by size, so the largest island in a + /// cluster always stays. Chebyshev distance between boundary cells. + /// + public float MinSeparationFrac = 0f; + + /// + /// ⚠ THE BLOB GUARD. An organic island LARGER than this fraction of the map's area is a + /// superlevel region that merged several maxima into one sprawling landmass — the "blob" + /// the developer does not want — and is reverted whole. Set well above the natural size at + /// the preset density so it is a safety net, not a sculptor; how often it bites is reported. + /// + public float MaxIslandAreaFrac = 0f; + // ---- the zone mask: protections + the Trench bound -------------------- /// ⭐ THE MOAT. Reference 14 m. The presets do not move it. @@ -115,95 +153,67 @@ namespace IslaApocalypse.Tools public float TrenchInner = IslandFalloff.OFFSHORE_TRENCH_INNER; public float TrenchOuter = IslandFalloff.OFFSHORE_TRENCH_OUTER; - // ---- the seeded floor (Hybrid only) ----------------------------------- - - /// ⭐ The guaranteed minimum island count per hemisphere. 0 / 0 ⇒ no floor (Faithful). - public int FloorNorth = 0; - public int FloorSouth = 0; - - /// Stamp radius, fraction of the map. Small and low is the target. - public float StampRadiusFrac = 0.012f; - - /// Fraction of the stamp radius that is flat top. Higher ⇒ flatter, crisper shore. - public float StampCoreFrac = 0.60f; - - /// - /// Radial jitter on the stamp's rim, as a fraction of the radius, from a fine noise field: - /// the outline wanders ±this much around the circle. 0 ⇒ a compass-drawn disc (the first - /// probe plate: rigid, but visibly geometric). 0.25 keeps the crisp shore and the flat top - /// while the coastline stops looking stamped. - /// - public float StampEdgeJitter = 0f; - - /// Minimum centre-to-centre separation between seeded islands, fraction of the map. - public float SeparationFrac = 0.08f; - - /// - /// Minimum clear water between a seeded stamp's rim and ANY existing land (mainland or - /// organic), fraction of the map. This is what makes every seeded island its own connected - /// component by construction — and therefore what makes the floor COUNT hold, not just the - /// number of stamps. - /// - public float LandGapFrac = 0.010f; - - /// The placement RNG's seed offset. A SEED offset; positions vary per world seed. - public int PlacementSeedOffset = 7703; - - /// Rejection-sampling budget per hemisphere before the pass REFUSES (loudly) rather than under-delivers. - public int MaxPlacementAttempts = 40000; - // ---- presets ---------------------------------------------------------- - /// The reference, verbatim. No floor, single density, original footprint/crest/mask. + /// The reference, verbatim. Single density, no guards, original footprint/crest/mask. public static OffshoreSettings Faithful() => new OffshoreSettings { Mode = OffshoreMode.Faithful, - Organic = true, FreqPerMapWidth = IslandFalloff.OFFSHORE_FREQ_ISLANDS, SeedOffset = IslandFalloff.OFFSHORE_SEED_OFFSET, - DensityNorth = 0.02f, DensitySouth = 0.02f, // ConfigManager.OffshoreIslandDensity + Density = 0.02f, SouthWeight = 1f, // ConfigManager.OffshoreIslandDensity CrestM = IslandFalloff.OFFSHORE_ISLAND_H_M, CoreFraction = IslandFalloff.OFFSHORE_CORE, EdgeSharpness = 1f, TrenchInner = IslandFalloff.OFFSHORE_TRENCH_INNER, TrenchOuter = IslandFalloff.OFFSHORE_TRENCH_OUTER, - FloorNorth = 0, FloorSouth = 0, }; /// - /// ⭐ The reshape: small / low / flatter / more rigid, loose-guaranteed (≥2 N, ≥4 S), organic - /// extras weighted south, corners and the outer edge allowed. + /// ⭐ THE DELIVERABLE (chat2/06) — the organic layer, reshaped (chat2/05 stage 2's shape: + /// small / low / flat / crisp, corners on) and tuned for coverage: density up from 05's + /// 0.007, south-weighted, guarded against specks / clusters / blobs. + /// + /// ⚠ TUNED BY MEASUREMENT, not by feel (chat2/06 batch — the count table over 12 seeds × + /// 3 levels and the per-hemisphere diagnosis). This preset is the batch's `density_mid`. /// - public static OffshoreSettings Hybrid() => new OffshoreSettings + public static OffshoreSettings Organic() => new OffshoreSettings { - Mode = OffshoreMode.Hybrid, - Organic = true, - // ⚠ Chosen by SWEEP, not by feel (chat2/05 scratch): at freq 24 / 0.010 / 0.025 the field - // produced 183 organic blobs of which 174 were debris. freq 16 with these densities gives - // the floor plus ~+1–2 N / +6–8 S genuine extras of a few hundred cells each at 2048. - FreqPerMapWidth = 16f, // a little smaller than the reference's 14 - DensityNorth = 0.007f, DensitySouth = 0.012f, // weighted south - CrestM = 24f, // lower than the reference's 34 (pre-curve) + Mode = OffshoreMode.Organic, + FreqPerMapWidth = 16f, // a little smaller than the reference's 14 (chat2/05) + Density = OrganicDensityMid, + SouthWeight = OrganicSouthWeight, + CrestM = 24f, // lower than the reference's 34 (pre-curve) — lands in the curve's preserved toe CoreFraction = 0.25f, // flatter than 0.45 EdgeSharpness = 2.5f, // crisper shore than the faithful smoothstep - MinIslandAreaFrac = 3e-5f, // no noise debris + MinIslandAreaFrac = OrganicMinIslandAreaFrac, + MinSeparationFrac = OrganicMinSeparationFrac, + MaxIslandAreaFrac = OrganicMaxIslandAreaFrac, TrenchInner = 0.90f, TrenchOuter = 0.97f, // corners + outer edge allowed; centre stays on-map - FloorNorth = 2, FloorSouth = 4, - StampRadiusFrac = 0.012f, StampCoreFrac = 0.60f, StampEdgeJitter = 0.25f, - SeparationFrac = 0.08f, LandGapFrac = 0.010f, }; + /// + /// The three density levels of the chat2/06 batch, in one place so the tool and the preset + /// cannot disagree. Mid is the preset of record. + /// + public const float OrganicDensityLow = 0.016f; // the edge: every seed of the 12 still clears N ≥ 2 / S ≥ 3, but N's minimum IS 2 + public const float OrganicDensityMid = 0.022f; // ⭐ the preset: N min 3 / mean 6.3, S min ~15 / mean ~20 over 12 seeds at 4096 + public const float OrganicDensityHigh = 0.030f; // the "how many is too many" bookend + public const float OrganicSouthWeight = 1.25f; // preference, not a fix — the diagnosis found the south UN-suppressed (see the report) + public const float OrganicMinIslandAreaFrac = 3e-5f; + public const float OrganicMinSeparationFrac = 0.008f; + public const float OrganicMaxIslandAreaFrac = 6e-4f; + public OffshoreSettings Clone() => (OffshoreSettings)MemberwiseClone(); public string Describe() { if (Mode == OffshoreMode.Off) return "offshore OFF"; var sb = new StringBuilder(); - sb.Append($"{Mode}: organic {(Organic ? "on" : "OFF")} freq {FreqPerMapWidth:F0}/map density N {DensityNorth:F3} S {DensitySouth:F3} · "); - sb.Append($"crest {CrestM:F0} m core {CoreFraction:F2} sharp {EdgeSharpness:F1} minArea {MinIslandAreaFrac:G2} · "); + sb.Append($"{Mode}: freq {FreqPerMapWidth:F0}/map density {Density:F4} × south {SouthWeight:F2} (= S {DensitySouth:F4}) · "); + sb.Append($"crest {CrestM:F0} m core {CoreFraction:F2} sharp {EdgeSharpness:F1} · "); + sb.Append($"guards minArea {MinIslandAreaFrac:G2} minSep {MinSeparationFrac:G2} maxArea {MaxIslandAreaFrac:G2} · "); sb.Append($"moat {MinDepthM:F0}+{DepthFeatherM:F0} m falloff {MinFalloff:F2}+{FalloffFeather:F2} trench {TrenchInner:F2}→{TrenchOuter:F2}"); - if (FloorNorth > 0 || FloorSouth > 0) - sb.Append($" · floor N{FloorNorth} S{FloorSouth} r {StampRadiusFrac:F3} core {StampCoreFrac:F2} sep {SeparationFrac:F2} gap {LandGapFrac:F3}"); return sb.ToString(); } } diff --git a/Tools/Scripts/Pass1Result.cs b/Tools/Scripts/Pass1Result.cs index 03172ba..27647ec 100644 --- a/Tools/Scripts/Pass1Result.cs +++ b/Tools/Scripts/Pass1Result.cs @@ -91,13 +91,13 @@ namespace IslaApocalypse.Tools /// public readonly byte[,] IslandHemisphere; - /// The seeded-floor stamp centres and radii (Hybrid only; empty otherwise). For the overlay and the report. - public readonly IReadOnlyList<(int x, int y, int r)> OffshoreCentres; - /// Cells the islet layer lifted above sea. The reference printed this too. public readonly long OffshoreLiftedCells; - /// Lines worth printing from the shelf/offshore pass: thresholds, floor placement, lifted counts. + /// The islet layer's numbers — thresholds, pre-guard count, guard ledger, final count. Null when offshore is off. + public readonly OffshoreLedger OffshoreLedger; + + /// Lines worth printing from the shelf/offshore pass: thresholds, guard ledger, lifted counts. public readonly IReadOnlyList Notes; /// Wall-clock milliseconds the pass took. @@ -107,8 +107,7 @@ namespace IslaApocalypse.Tools float[,] latitudeField, float hMaxSeed, float hMinSeed, ulong elapsedMs, float hMaxSeedBeforeOffshore = float.NaN, bool[,] isOffshoreIsland = null, byte[,] islandHemisphere = null, - IReadOnlyList<(int x, int y, int r)> offshoreCentres = null, - long offshoreLiftedCells = 0, IReadOnlyList notes = null) + long offshoreLiftedCells = 0, IReadOnlyList notes = null, OffshoreLedger offshoreLedger = null) { MapSize = mapSize; Seed = seed; @@ -121,8 +120,8 @@ namespace IslaApocalypse.Tools HMaxSeedBeforeOffshore = float.IsNaN(hMaxSeedBeforeOffshore) ? hMaxSeed : hMaxSeedBeforeOffshore; IsOffshoreIsland = isOffshoreIsland; IslandHemisphere = islandHemisphere; - OffshoreCentres = offshoreCentres ?? System.Array.Empty<(int, int, int)>(); OffshoreLiftedCells = offshoreLiftedCells; + OffshoreLedger = offshoreLedger; Notes = notes ?? System.Array.Empty(); } diff --git a/Tools/Scripts/ShapingOracle.cs b/Tools/Scripts/ShapingOracle.cs index 9ea805c..9fa73df 100644 --- a/Tools/Scripts/ShapingOracle.cs +++ b/Tools/Scripts/ShapingOracle.cs @@ -402,28 +402,9 @@ namespace IslaApocalypse.Tools // ═══ chat2/05 — the offshore checks ═══ - /// - /// (h) ⭐ FLOOR HOLDS — at least / - /// offshore islands per hemisphere, counted as 8-connected components of TAGGED land by - /// centroid. The pass already refused to return without this; the oracle proves it again, - /// independently, on the finished field — a guarantee checked once is a guarantee checked by - /// the thing that might be wrong. - /// - public static Check OffshoreFloor(Pass1Result p1, int needNorth, int needSouth, float sea, - List comps) - { - var c = new Check { Id = "h", Name = $"offshore floor ≥{needNorth} N / ≥{needSouth} S" }; - if (!p1.HasOffshoreTag) - { - c.Passed = false; - c.Detail = "no offshore tag on this field — offshore was off."; - return c; - } - var (n, s) = OffshoreAnalysis.CountByHemisphere(comps); - c.Passed = n >= needNorth && s >= needSouth; - c.Detail = $"{n} north, {s} south ({comps.Count} islands)"; - return c; - } + // (h) was the chat2/05 seeded-floor check — reverted out with the floor in chat2/06. No + // count is guaranteed any more, so there is nothing for an oracle to assert; the count + // table is the evidence, and it is statistics, not a check. /// /// (i) ⭐ MOAT INTACT — no offshore island is 8-connected to mainland land. The moat exists diff --git a/Tools/Scripts/TagOverlayRenderer.cs b/Tools/Scripts/TagOverlayRenderer.cs index 4b3d273..f5d388f 100644 --- a/Tools/Scripts/TagOverlayRenderer.cs +++ b/Tools/Scripts/TagOverlayRenderer.cs @@ -1,11 +1,10 @@ -using System.Collections.Generic; using Godot; namespace IslaApocalypse.Tools { /// /// The offshore TAG / HEMISPHERE debug overlay (chat2/05): mainland one tint, offshore-island - /// land tinted by hemisphere, seeded centres ringed, the midline drawn — so the island count, + /// land tinted by hemisphere, the midline drawn — so the island count, /// the N/S split and the tag's correctness are all visible at one glance. /// /// ⚠ A DIAGNOSTIC, NOT A MAP. It draws the tag layer, which is DATA the shape pass set; it is @@ -22,7 +21,6 @@ namespace IslaApocalypse.Tools private static readonly Color IslandS = new(0.980f, 0.600f, 0.200f); // warm — south private static readonly Color Untagged = new(0.950f, 0.150f, 0.800f); // ⚠ land that is neither — must never appear private static readonly Color Midline = new(0.700f, 0.720f, 0.760f); - private static readonly Color Ring = new(1.000f, 1.000f, 1.000f); private static readonly Color Ink = new(0.941f, 0.949f, 0.961f); /// @@ -30,7 +28,7 @@ namespace IslaApocalypse.Tools /// by mis-tagging mainland). Null ⇒ derived as "land and not tagged", which is weaker. /// public static void SavePng(float[,] height, bool[,] tag, byte[,] hemi, int mapSize, float sea, - IReadOnlyList<(int x, int y, int r)> centres, int countN, int countS, string absolutePath) + int countN, int countS, string absolutePath) { var img = Image.CreateEmpty(mapSize, mapSize, false, Image.Format.Rgb8); @@ -59,38 +57,16 @@ namespace IslaApocalypse.Tools int mid = mapSize / 2; for (int x = 0; x < mapSize; x += 3) img.SetPixel(x, mid, Midline); - // Seeded centres: a ring at the stamp radius, so the floor islands can be told from the - // organic ones by eye. - if (centres != null) - foreach (var (cx, cy, r) in centres) - DrawRing(img, cx, cy, r, mapSize); - // A legend that cannot be separated from the picture. int s = mapSize >= 4096 ? 4 : 3; int lh = TinyFont.Height(s) + 6; TinyFont.Draw(img, "OFFSHORE TAG OVERLAY", 12, 12, s, Ink); TinyFont.Draw(img, "GREY: MAINLAND CYAN: ISLAND N ORANGE: ISLAND S", 12, 12 + lh, s, Ink); - TinyFont.Draw(img, $"ISLANDS: {countN} NORTH {countS} SOUTH - RINGS: SEEDED FLOOR", 12, 12 + lh * 2, s, Ink); + TinyFont.Draw(img, $"ISLANDS: {countN} NORTH {countS} SOUTH - ALL ORGANIC, NONE FORCED", 12, 12 + lh * 2, s, Ink); TinyFont.Draw(img, "N ABOVE THE LINE - S BELOW - Y RUNS SOUTH", 12, 12 + lh * 3, s, Ink); Error err = img.SavePng(absolutePath); if (err != Error.Ok) GD.PrintErr($"[TagOverlayRenderer] SavePng failed ({err}) for {absolutePath}"); } - - private static void DrawRing(Image img, int cx, int cy, int r, int n) - { - int rr = r + 3; // just outside the rim - int steps = System.Math.Max(64, rr * 4); - for (int i = 0; i < steps; i++) - { - double a = i * 2.0 * System.Math.PI / steps; - for (int t = 0; t < 2; t++) // 2 px thick - { - int px = cx + (int)System.Math.Round((rr + t) * System.Math.Cos(a)); - int py = cy + (int)System.Math.Round((rr + t) * System.Math.Sin(a)); - if (px >= 0 && py >= 0 && px < n && py < n) img.SetPixel(px, py, Ring); - } - } - } } } diff --git a/Tools/Scripts/TerrainGenConfig.cs b/Tools/Scripts/TerrainGenConfig.cs index f5ca742..03a3d2d 100644 --- a/Tools/Scripts/TerrainGenConfig.cs +++ b/Tools/Scripts/TerrainGenConfig.cs @@ -270,7 +270,7 @@ namespace IslaApocalypse.Tools /// /// ⭐ The offshore islet system — every dial in one object. Mode = Off by default /// (see the note above). is the reference verbatim; - /// is the reshape. + /// is the reshape, tuned (chat2/06). /// public OffshoreSettings Offshore = new OffshoreSettings(); diff --git a/Tools/Scripts/Topography.cs b/Tools/Scripts/Topography.cs index 0adc131..36166fe 100644 --- a/Tools/Scripts/Topography.cs +++ b/Tools/Scripts/Topography.cs @@ -283,7 +283,7 @@ namespace IslaApocalypse.Tools // The reference's pass 1 CONTINUED HERE with the coast shelf (~:621-640) and the // offshore islets (~:641-664). v2 runs them as PASS 1b, a second sweep over these // arrays, immediately below — same per-pixel arithmetic, same order, and the - // seeded floor needs the whole depth/falloff field to exist first. → OffshorePass. + // slop guards need the whole field to see whole islands. → OffshorePass. } } @@ -317,9 +317,9 @@ namespace IslaApocalypse.Tools hMaxSeedBeforeOffshore: hMaxBeforeOffshore, isOffshoreIsland: offshore?.Tag, islandHemisphere: offshore?.Hemi, - offshoreCentres: offshore?.Centres, - offshoreLiftedCells: offshore == null ? 0 : offshore.LiftedOrganic + offshore.LiftedSeeded - offshore.LiftedReverted, - notes: offshore?.Notes); + offshoreLiftedCells: offshore == null ? 0 : offshore.LiftedOrganic - offshore.LiftedReverted, + notes: offshore?.Notes, + offshoreLedger: offshore?.ToLedger()); } } }