From d4b46afe0071483f66f485f04dcda93d5c3f6820 Mon Sep 17 00:00:00 2001 From: Jonas Reith Date: Sun, 28 Jun 2026 13:42:17 +0200 Subject: [PATCH] Cap tides on small enclosed seas (inland lakes stay calm) A small closed-off ocean body cannot build a real tidal range -- the equilibrium tide assumes a connected global ocean. computeTides now flood-fills connected ocean bodies and caps any body under 10 cells to 0.01 m/cell + 0.03 m (a one-cell sea ~0.04 m); open oceans (>=10 cells) keep the full equilibrium tide. The Sky & tides panel reads the capped level so it stays consistent with the cell-info panel. test_ocean.cpp covers both cases. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 5 ++++- src/render/ViewerRender.cpp | 10 ++++++---- src/sim/PlanetOcean.cpp | 25 +++++++++++++++++++++++++ test_ocean.cpp | 17 +++++++++++++++++ 4 files changed, 52 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index bc6adec..7937093 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -356,7 +356,10 @@ Working and verified (logic tested headless): v9** appends the moons block (`writeState`/`readState(...,hasMoons)`; pre-v9 saves synthesize moons from the seed). `test_ocean.cpp`: moon count/determinism + RNG isolation, unit sweeping sky dirs, zero-mean two-bulge tide (high under moon + antipode, low at 90°, moves with time), - save v9 round-trip. + save v9 round-trip. **Enclosed-sea cap:** `computeTides` flood-fills connected ocean bodies and + caps the amplitude of any body under 10 cells to `0.01·cells + 0.03` m (a one-cell sea ≈ 0.04 m, + an inland saltwater lake stays calm) — a small closed basin can't build a real tidal range; + open oceans (≥10 cells) keep the full equilibrium tide. - **Live World — ocean currents + climate feedback:** `Planet::computeOceanCurrents()` (PlanetOcean.cpp) builds a per-ocean-cell tangent velocity `sCurrent` (derived/not saved): wind stress (`sWind`) rotated by a **Coriolis** deflection (right N / left S about the cell diff --git a/src/render/ViewerRender.cpp b/src/render/ViewerRender.cpp index 91b8934..3397a5b 100644 --- a/src/render/ViewerRender.cpp +++ b/src/render/ViewerRender.cpp @@ -290,11 +290,13 @@ void Viewer::renderLiveInfo() { h += planet.cfg.tideSunFactor * (cs * cs - 1.0 / 3.0); return h * planet.cfg.tideAmplitude; }; - double t0 = cellTide(doy, tod, days), t1 = cellTide(doy2, tod2, days2); - bool rising = t1 >= t0; + bool rising = cellTide(doy2, tod2, days2) >= cellTide(doy, tod, days); // direction + // Level from the actual tide field (so the enclosed-sea cap is reflected here too). + double lvl = ((int)planet.tide().size() == (int)planet.cells.size()) ? planet.tide()[selectedCell] + : cellTide(doy, tod, days); DrawText(TextFormat("coastal cell #%d", selectedCell), x, y, 15, Color{160, 170, 185, 255}); y += 21; - DrawText(TextFormat("%+.2f m %s, %s", t0, t0 >= 0.0 ? "high" : "low", rising ? "rising" : "falling"), - x, y, 17, tideColor(t0, std::max(0.05, std::fabs(t0)))); y += 24; + DrawText(TextFormat("%+.2f m %s, %s", lvl, lvl >= 0.0 ? "high" : "low", rising ? "rising" : "falling"), + x, y, 17, tideColor(lvl, std::max(0.05, std::fabs(lvl)))); y += 24; DrawText("(equilibrium model - placeholder)", x, y, 13, Color{120, 125, 140, 255}); } else { DrawText("selected tile is inland", x, y, 15, Color{150, 155, 170, 255}); diff --git a/src/sim/PlanetOcean.cpp b/src/sim/PlanetOcean.cpp index 0c2f372..59d5698 100644 --- a/src/sim/PlanetOcean.cpp +++ b/src/sim/PlanetOcean.cpp @@ -149,4 +149,29 @@ void Planet::computeTides(double dayOfYear01, double timeOfDay01, double timeDay } sTide[i] = amp * h; } + + // Enclosed-sea correction: a small, closed-off ocean body (an inland saltwater lake or a + // one-tile sea) cannot build a real tidal range -- the equilibrium tide assumes a connected + // global ocean. Cap the amplitude of any ocean body under `enclosedMax` cells to + // 0.01 m/cell + 0.03 m (1 cell -> ~0.04 m, scaling up to the threshold); larger / open + // oceans keep the full equilibrium tide. Components are flood-filled over the fixed grid. + const int enclosedMax = 10; + const double sea = cfg.seaLevel; + std::vector seen(n, 0); + std::vector stack, members; + for (int i = 0; i < n; ++i) { + if (cells[i].elevation > sea || seen[i]) continue; + stack.clear(); members.clear(); + stack.push_back(i); seen[i] = 1; + while (!stack.empty()) { + int c = stack.back(); stack.pop_back(); members.push_back(c); + for (int nb : cells[c].neighbors) + if (cells[nb].elevation <= sea && !seen[nb]) { seen[nb] = 1; stack.push_back(nb); } + } + int sz = (int)members.size(); + if (sz < enclosedMax) { + double cap = 0.01 * sz + 0.03; + for (int m : members) sTide[m] = std::clamp(sTide[m], -cap, cap); + } + } } diff --git a/test_ocean.cpp b/test_ocean.cpp index b01ada7..34ee9bc 100644 --- a/test_ocean.cpp +++ b/test_ocean.cpp @@ -128,6 +128,23 @@ int main() { } check(det, "currents + feedback deterministic for a seed"); + std::printf("Ocean: enclosed-sea tide cap\n"); + { + // A single isolated ocean cell (closed-off sea) must have a tiny tidal range. + Planet e; e.generate(cfg); + for (auto& cc : e.cells) cc.elevation = 100.0; // all land + e.cells[0].elevation = -100.0; // one-cell sea + e.computeTides(0.0, 0.3, 4.0); + check(std::fabs(e.tide()[0]) <= 0.04 + 1e-9, "a one-cell sea is capped to ~0.04 m"); + + // A fully open ocean (one giant body) keeps the full equilibrium tidal range. + Planet g; g.generate(cfg); + for (auto& cc : g.cells) cc.elevation = -100.0; // all ocean + g.computeTides(0.0, 0.3, 4.0); + double gmax = 0.0; for (int i = 0; i < n; ++i) gmax = std::max(gmax, std::fabs(g.tide()[i])); + check(gmax > 0.1, "a large open ocean keeps the full tidal range"); + } + std::printf("Ocean: save v9\n"); std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary); planet.writeState(ss);